Wed, 06 Dec 2017 17:14:02 +0200
Updated documentation and cleanup of the LidarChannel class.
atmospheric_lidar/generic.py | file | annotate | diff | comparison | revisions |
--- a/atmospheric_lidar/generic.py Wed Dec 06 13:36:42 2017 +0200 +++ b/atmospheric_lidar/generic.py Wed Dec 06 17:14:02 2017 +0200 @@ -21,7 +21,7 @@ You can override the set_PT method to define a custom procedure to get ground temperature and pressure. - The class assumes that the input files are consequtive, i.e. there are no measurements gaps. + The class assumes that the input files are consecutive, i.e. there are no measurements gaps. """ def __init__(self, file_list=None): """ @@ -237,7 +237,7 @@ def subset_by_bins(self, b_min=0, b_max=None): """ - Remove some height bins from the file. + Remove some height bins from the measurement data. This could be needed to remove acquisition artifacts at the first or last bins of the profiles. @@ -650,8 +650,26 @@ class LidarChannel: + """ + This class represents a general measurement channel, independent of the input files. + + The class assumes that the input files are consecutive, i.e. there are no measurements gaps. + """ + def __init__(self, channel_parameters): - c = 299792458 # Speed of light + """ + This is run when creating a new object. + + The input dictionary should contain 'name', 'binwidth', and 'data' keys. + + Parameters + ---------- + channel_parameters : dict + A dict containing channel parameters. + """ + # TODO: Change channel_parameters to explicit keyword arguments? + + c = 299792458. # Speed of light self.wavelength = channel_parameters['name'] self.name = str(self.wavelength) self.binwidth = float(channel_parameters['binwidth']) # in microseconds @@ -664,35 +682,80 @@ self.duration = 60 def calculate_rc(self, idx_min=4000, idx_max=5000): + """ Calculate range corrected signal. + + The background signal is estimated as the mean signal between idx_min and idx_max. + + The calculations is stored in the self.rc parameters. + + Parameters + ---------- + idx_min : int + Minimum index to calculate background signal. + idx_max : int + Maximum index to calculate background signal. + """ background = np.mean(self.matrix[:, idx_min:idx_max], axis=1) # Calculate the background from 30000m and above self.rc = (self.matrix.transpose() - background).transpose() * (self.z ** 2) def update(self): + """ + Update the time parameters and data according to the raw input data. + """ self.start_time = min(self.data.keys()) self.stop_time = max(self.data.keys()) + datetime.timedelta(seconds=self.duration) self.time = tuple(sorted(self.data.keys())) sorted_data = sorted(self.data.iteritems(), key=itemgetter(0)) self.matrix = np.array(map(itemgetter(1), sorted_data)) - def _nearest_dt(self, dtime): - margin = datetime.timedelta(seconds=300) - if ((dtime + margin) < self.start_time) | ((dtime - margin) > self.stop_time): + def _nearest_datetime(self, input_time): + """ + Find the profile datetime and index that is nearest to the given datetime. + + Parameters + ---------- + input_time : datetime.datetime + Input datetime object. + + Returns + ------- + profile_datetime : datetime + The datetime of the selected profile. + profile_idx : int + The index of the selected profile. + """ + margin = datetime.timedelta(seconds=self.duration * 5) + + if ((input_time + margin) < self.start_time) | ((input_time - margin) > self.stop_time): logging.error("Requested date not covered in this file") raise ValueError("Requested date not covered in this file") - dt = abs(self.time - np.array(dtime)) - dtmin = min(dt) + dt = np.abs(np.array(self.time) - input_time) + + profile_idx = np.argmin(dt) + profile_datetime = self.time[profile_idx] - if dtmin > datetime.timedelta(seconds=60): - logging.warning("Nearest profile more than 60 seconds away. dt = %s." % dtmin) - ind_t = np.where(dt == dtmin) - ind_a = ind_t[0] - if len(ind_a) > 1: - ind_a = ind_a[0] - chosen_time = self.time[ind_a] - return chosen_time, ind_a + dt_min = dt[profile_idx] + if dt_min > datetime.timedelta(seconds=self.duration): + logging.warning("Nearest profile more than 60 seconds away. dt = %s." % dt_min) + + return profile_datetime, profile_idx def subset_by_time(self, start_time, stop_time): + """ + Subset the channel for a specific time interval. + Parameters + ---------- + start_time : datetime + The starting datetime to subset. + stop_time : datetime + The stopping datetime to subset. + + Returns + ------- + c : LidarChannel object + A new channel object + """ time_array = np.array(self.time) condition = (time_array >= start_time) & (time_array <= stop_time) @@ -706,16 +769,32 @@ # We should use __class__ instead of class name, so that this works properly # with subclasses - # Ex: c = self.__class__(parameters_values) + # Eg: c = self.__class__(parameters_values) # This does not currently work with Licel files though + # TODO: Fix this! c = LidarChannel(parameter_values) c.data = subset_data c.update() return c def subset_by_bins(self, b_min=0, b_max=None): - """Remove some height bins from the file. This could be needed to - remove acquisition artifacts at the start or the end of the files. + """ + Remove some height bins from the channel data. + + This could be needed to remove acquisition artifacts at + the first or last bins of the profiles. + + Parameters + ---------- + b_min : int + The fist valid data bin + b_max : int or None + The last valid data bin. If equal to None, all bins are used. + + Returns + ------- + m : LidarChannel object + A new channel object """ subset_data = {} @@ -734,50 +813,106 @@ c.update() return c - def profile(self, dtime, signal_type='rc'): - t, idx = self._nearest_dt(dtime) - if signal_type == 'rc': + def get_profile(self, input_datetime, range_corrected=True): + """ Returns a single profile, that is nearest to the input datetime. + + Parameters + ---------- + input_datetime : datetime + The required datetime of the profile + range_corrected : bool + If True, the range corrected profile is returned. Else, normal signal is returned. + + Returns + ------- + profile : ndarray + The profile nearest to input_datetime. + t : datetime + The actual datetime corresponding to the profile. + """ + t, idx = self._nearest_datetime(input_datetime) + if range_corrected: data = self.rc else: data = self.matrix - prof = data[idx, :][0] - return prof, t + profile = data[idx, :][0] + + return profile, t + + def get_slice(self, start_time, stop_time, range_corrected=True): + """ Returns a slice of data, between the provided start and stop time. - def get_slice(self, starttime, endtime, signal_type='rc'): - if signal_type == 'rc': + Parameters + ---------- + start_time : datetime + The starting time of the slice + stop_time : datetime + The stoping time of the slice + range_corrected : bool + If True, the range corrected profile is returned. Else, normal signal is returned. + + Returns + ------- + slice : ndarray + The slice of profiles. + slice_datetimes : ndarray of datetime + The actual datetimes corresponding to the slice. + """ + if range_corrected: data = self.rc else: data = self.matrix - tim = np.array(self.time) - starttime = self._nearest_dt(starttime)[0] - endtime = self._nearest_dt(endtime)[0] - condition = (tim >= starttime) & (tim <= endtime) - sl = data[condition, :] - t = tim[condition] - return sl, t + + time_array = np.array(self.time) + start_time = self._nearest_datetime(start_time)[0] + stop_time = self._nearest_datetime(stop_time)[0] - def profile_for_duration(self, tim, duration=datetime.timedelta(seconds=0), signal_type='rc'): - """ Calculates the profile around a specific time (tim). """ - starttime = tim - duration / 2 - endtime = tim + duration / 2 - d, t = self.get_slice(starttime, endtime, signal_type=signal_type) - prof = np.mean(d, axis=0) - tmin = min(t) - tmax = max(t) - tav = tmin + (tmax - tmin) / 2 - return prof, (tav, tmin, tmax) + condition = (time_array >= start_time) & (time_array <= stop_time) + + slice = data[condition, :] + slice_datetimes = time_array[condition] + + return slice, slice_datetimes def average_profile(self): - """ Return the average profile (NOT range corrected) for all the duration of the measurement. """ - prof = np.mean(self.matrix, axis=0) - return prof + """ Return the average profile (NOT range corrected) for all the duration of + the measurement. + + Returns + ------- + profile : ndarray + The average profile for all data. + """ + profile = np.mean(self.matrix, axis=0) + return profile - def plot(self, figsize=(8, 4), signal_type='rc', zoom=[0, 12000, 0, -1], show_plot=True, cmap=plt.cm.jet, z0=None, - title=None, vmin=0, vmax=1.3 * 10 ** 7): - # if filename is not None: - # matplotlib.use('Agg') - + def plot(self, figsize=(8, 4), signal_type='rc', zoom=[0, 12000, 0, -1], show_plot=True, + cmap=plt.cm.jet, z0=None, title=None, vmin=0, vmax=1.3 * 10 ** 7): + """ + Plot of the channel data. + + Parameters + ---------- + figsize : tuple + (width, height) of the output figure (inches) + signal_type : str + If 'rc', the range corrected signal is ploted. Else, the raw signals are used. + zoom : list + A four elemet list defined as [xmin, xmax, ymin, ymax]. Use ymin=0, ymax=-1 to plot full range. + show_plot : bool + If True, the show_plot command is run. + cmap : cmap + An instance of a matplotlib colormap to use. + z0 : float + The ground-level altitude. If provided the plot shows altitude above sea level. + title : str + Optional title for the plot. + vmin : float + Minimum value for the color scale. + vmax : float + Maximum value for the color scale. + """ fig = plt.figure(figsize=figsize) ax1 = fig.add_subplot(111) @@ -795,7 +930,32 @@ zoom=[0, 12000, 0, -1], z0=None, add_colorbar=True, cmap_label='a.u.', cb_format=None, vmin=0, vmax=1.3 * 10 ** 7): - + """ + Draw channel data on the given axis. + + Parameters + ---------- + ax1 : axis object + The axis object to draw. + cmap : cmap + An instance of a matplotlib colormap to use. + signal_type : str + If 'rc', the range corrected signal is ploted. Else, the raw signals are used. + zoom : list + A four elemet list defined as [xmin, xmax, ymin, ymax]. Use ymin=0, ymax=-1 to plot full range. + z0 : float + The ground-level altitude. If provided the plot shows altitude above sea level. + add_colorbar : bool + If True, a colorbar will be added to the plot. + cmap_label : str + Label for the colorbar. Ignored if add_colorbar is False. + cb_format : str + + vmin : float + Minimum value for the color scale. + vmax : float + Maximum value for the color scale. + """ if signal_type == 'rc': if len(self.rc) == 0: self.calculate_rc() @@ -862,6 +1022,14 @@ date_labels=False, vmin=0, vmax=1.3 * 10 ** 7): + """ + Updated drawing routine, using pcolormesh. It is slower but provides more flexibility / precision + in time plotting. The 2 drawing routines should be merged. + + Check draw_plot method for the meaning of the input arguments. + """ + # TODO: Merge the two drawing routines. + if signal_type == 'rc': if len(self.rc) == 0: self.calculate_rc() @@ -934,6 +1102,19 @@ cb1.ax.yaxis.get_offset_text().set_fontsize(cb_font_size) def _index_at_height(self, height): + """ + Get the altitude index nearest to the specified height. + + Parameters + ---------- + height : float + Height (m) + + Returns + ------- + idx : int + Index corresponding to the provided height. + """ idx = np.array(np.abs(self.z - height).argmin()) if idx.size > 1: idx = idx[0]