diff --git a/aosstower/level_b1/nc.py b/aosstower/level_b1/nc.py index 0f696a4e36817546e6450326e4850c4c1194f541..f4e472c915b22026a9cb8c875bbc8ed4d1ec7d34 100644 --- a/aosstower/level_b1/nc.py +++ b/aosstower/level_b1/nc.py @@ -151,8 +151,8 @@ def create_variables(nc_file, first_stamp, database, chunk_sizes=None, zlib=Fals variable.ancillary_variables = 'base_time' # CF default - # if 'time' in key: - # variable.calendar = 'gregorian' + if 'time' in key: + variable.calendar = 'gregorian' for entry in sorted(database.keys()): if entry == 'stamp': diff --git a/aosstower/tower_quicklooks/create_quicklook.py b/aosstower/tower_quicklooks/create_quicklook.py index 7873f03171e65aeaaa477b9f65d31480d3908768..e8da206127711847555ec727006368c150e68712 100644 --- a/aosstower/tower_quicklooks/create_quicklook.py +++ b/aosstower/tower_quicklooks/create_quicklook.py @@ -1,6 +1,6 @@ import os -from datetime import datetime as dt -from datetime import timedelta as delta +import sys +from datetime import datetime as dt, timedelta as delta import logging import pandas as pd from netCDF4 import MFDataset, MFTime @@ -8,99 +8,184 @@ import numpy as np import matplotlib.pyplot as plt import math -CHOICES = ['air_temp', 'dewpoint', - 'rh', 'pressure', 'wind_speed', 'wind_dir', 'accum_precip', 'solar_flux'] - -TITLES = {'air_temp': 'Temperature and Dewpoint(°C)', - 'rh': 'Relative Humidity(%)', 'pressure': 'Pressure(hpa)', - 'wind_speed': 'Wind Speed(m/s)', 'wind_dir': 'Wind Direction(°)', - 'accum_precip': 'Accumulated Precipitation Since 00Z (mm)', - 'solar_flux': 'Solar Flux(W/m^2)'} - -IND_TITLES = {'air_temp': 'Temperature and Dewpoint', - 'rh': 'Relative Humidity', 'pressure': 'Pressure', - 'wind_speed': 'Wind Speed', 'wind_dir': 'Wind Direction', - 'accum_precip': 'Accumulated Precipitation Since 00Z', - 'solar_flux': 'Solar Flux'} - -# The purpose of this method is to take a string in the format -# YYYY-MM-DDTHH:MM:SS and convert that to a datetime object -# or a string in the format YY-MM-DD and convert that to a datetime object -# used in coordination with argparse -s and -e params -# @param datetime string -# @return datetime object - -def _dt_convert(datetime_str): - #parse datetime string, return datetime object - try: - return dt.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S') - - except: - return dt.strptime(datetime_str, '%Y-%m-%d') - -# The purpose of this method is to take a list of level_b1 filepaths -# and convert those into a pandas frame with only good data -# @param input_files - list of level_b1 filepaths -# @return frame - pandas dataframe with only good data +LOG = logging.getLogger(__name__) + +# names of the plots used in title (default is `.title()` of plot name) +TITLES = { + 'air_temp': 'Air Temperature', + 'td': 'Air and Dewpoint Temperature', + 'rh': 'Relative Humidity', + 'wind_dir': 'Wind Direction', + 'accum_precip': 'Accumulated Precipitation Since 0Z', +} + + +class PlotMaker(object): + """Object for making plots and storing/validating plot metadata""" + def __init__(self, name, dependencies, title=None, units=None): + self.name = name + self.deps = dependencies + self._full_figure = None + if title is None: + title = "{title_prefix}{title_name}{units} {start_time:%Y-%m-%d}" + self._title = title + self.units = units + + def missing_deps(self, frame): + """Get dependency variables missing from the provided frame""" + for var_name in self.deps: + if var_name not in frame: + yield var_name + + def get_title(self, frame, is_subplot): + if self._title: + title_prefix = "AO&SS Building Tower " if not is_subplot else '' + title_name = TITLES.get(self.name, self.name.replace('_', ' ').title()) + unit_str = '({})'.format(self.units) if self.units and is_subplot else '' + title = self._title.format(title_prefix=title_prefix, + title_name=title_name, + units=unit_str, + start_time=frame.index[0].to_pydatetime()) + else: + title = '' + return title + + def get_yticks(self, ymin, ymax, num_plots): + delta = math.ceil((ymax - ymin) / num_plots) + new_ticks = np.arange(ymin, (ymin + delta * num_plots), delta) + if not new_ticks: + return [ymin, ymin + 0.05, ymin + 0.1] + return new_ticks + + def get_ylabel(self, is_subplot=False): + y_label = TITLES.get(self.name, self.name.replace('_', ' ').title()) + if is_subplot: + return None + if self.units: + return "{} ({})".format(y_label, self.units) + return y_label + + def create_plot(self, frame, fig, is_subplot=None, shared_x=None, title=None): + """ + + :param frame: + :param fig: + :param is_subplot: None or (num plots, num columns, num_rows) + :param shared_x: + :return: + """ + if title is None: + title = self.get_title(frame, is_subplot) + + if is_subplot: + ax = fig.add_subplot(*is_subplot, sharex=shared_x) + ax.set_title(title, x=0.5, y=get_subtitle_location(is_subplot[0]), fontsize=8) + else: + ax = fig.add_subplot(111, sharex=shared_x) + fig.suptitle(title, fontsize=13) + y_label = self.get_ylabel(is_subplot) + if y_label: + ax.set_ylabel(y_label) + plt.sca(ax) + + # get the min for each column then combine them assuming we can + specific_frame = frame[[x for x in frame.columns if x in self.deps]] + ymin = np.floor(specific_frame.min().min()) + ymax = np.ceil(specific_frame.max().max()) + + ax.plot(specific_frame.index, specific_frame, 'k') + if ymin == ymax: + ax.set_ylim(ymin, ymax + 0.1) + else: + ax.set_ylim(ymin, ymax) -def get_data(input_files): + if is_subplot: + new_ticks = self.get_yticks(ymin, ymax, is_subplot[0]) + ax.get_yaxis().get_major_ticks()[-1].set_visible(False) + ax.set_yticks(new_ticks) + else: + ax.set_xlabel('Time (UTC)') + + return ax + + +class MeteorogramPlotMaker(PlotMaker): + def __init__(self, name, dependencies, plot_deps=None, title=None): + self.plot_deps = plot_deps + super(MeteorogramPlotMaker, self).__init__(name, dependencies, title=title) + + def create_plot(self, frame, fig, is_subplot=False, shared_x=None): + if is_subplot or shared_x: + raise ValueError("Meteorogram Plot can not be a subplot or share X-axis") + + title = self.get_title(frame, False) + fig.suptitle(title, fontsize=13) + + num_plots = len(self.plot_deps) + shared_x = None + for idx, plot_name in enumerate(self.plot_deps): + plot_maker = PLOT_TYPES.get(plot_name, PlotMaker(plot_name, (plot_name,))) + title_name = TITLES.get(plot_name, plot_name.replace('_', ' ').title()) + ax = plot_maker.create_plot(frame, fig, + is_subplot=(num_plots, 1, idx + 1), + shared_x=shared_x, + title=title_name) + if idx == 0: + shared_x = ax + if idx != num_plots - 1: + # Disable the x-axis ticks so we don't interfere with other subplots + ax.get_xaxis().get_major_ticks()[-1].set_visible(False) + ax.get_xaxis().get_major_ticks()[0].set_visible(False) + ax.set_xlabel('Time (UTC)') + + return ax + +# map plot name -> variable dependencies +# if not listed then plot name is assumed to be the same as the variable needed +PLOT_TYPES = { + 'meteorogram': MeteorogramPlotMaker('meteorogram', + ('air_temp', 'dewpoint', 'rh', 'wind_speed', 'wind_dir', 'accum_precip'), + ('td', 'rh', 'wind_speed', 'wind_dir', 'accum_precip')), + 'td': PlotMaker('td', ('air_temp', 'dewpoint'), units="°C"), # air_temp and dewpoint in one plot + 'wind_dir': PlotMaker('wind_dir', ('wind_dir',), units='°'), # special tick labels + 'rh': PlotMaker('rh', ('rh',), units='%'), + 'air_temp': PlotMaker('air_temp', ('air_temp',), units='°C'), + 'pressure': PlotMaker('pressure', ('pressure',), units='hpa'), + 'dewpoint': PlotMaker('dewpoint', ('air_temp',), units='°C'), + 'wind_speed': PlotMaker('wind_speed', ('wind_speed',), units='m/s'), + 'accum_precip': PlotMaker('accum_precip', ('accum_precip',), units='mm'), + 'solar_flux': PlotMaker('solar_flux', ('solar_flux',), units='W/m^2'), +} + + +def convert_to_thumbnail(fig): + # TODO + pass + + +def get_data(input_files, columns): + data_dict = {} files = MFDataset(input_files) - #dictionary with var_name: good data pairs - dataDict = {} - - #get the data from the files - for name in CHOICES: - data = files.variables[name][:] - - qc_data = files.variables['qc_' + name][:] - - dataDict[name] = data - dataDict['qc_' + name] = qc_data + # get the data from the files + for name in columns: + if name not in files.variables: + LOG.warning("Unknown file variable: {}".format(name)) + continue + data_dict[name] = files.variables[name][:] + data_dict['qc_' + name] = files.variables['qc_' + name][:] - #for some reason, time has floating point precision issues - #so I'm using base_time and offsets instead - #might need to fix later + # convert base_time epoch format into date_time object base_time = files.variables['base_time'][:] - - #convert base_time epoch format into date_time object - base_time_obj = dt(1970,1,1) + delta(seconds=int(base_time)) + base_time_obj = dt(1970, 1, 1) + delta(seconds=int(base_time)) - #get offsets from files - off_sets = files.variables['time_offset'] - - #convert that into offsets from the first file's base_time - off_sets = MFTime(off_sets)[:] - - #for each offset, convert that into a datetime object - stamps = [base_time_obj + delta(seconds=int(s)) for s in off_sets] + # convert per-file offsets to offsets based on the first file's base_time + offsets = MFTime(files.variables['time_offset'])[:] + # for each offset, convert that into a datetime object + data_dict['stamps'] = [base_time_obj + delta(seconds=int(s)) for s in offsets] - #append all stamps into the data frame - dataDict['stamps'] = stamps + return pd.DataFrame(data_dict).set_index(['stamps']) - return pd.DataFrame(dataDict).set_index(['stamps']) - -# The purpose of this method is to use the qc mask -# to only get the good data -# @param qc_data - np array with nan for good values and various set bits for others -# @param stamps - all stamps corresponding to data -# @param data - data -# @return good data and their corresponding stamps - -def get_good_data(qc_data, stamps, data): - #all good data and stamps associated with them - good_data = [] - good_stamps = [] - - #get good indices - good_indices = np.where(np.isnan(qc_data))[0] - - for idx in good_indices: - #get good_data - good_data.append(data[idx]) - good_stamps.append(stamps[idx]) - - return [good_data, good_stamps] # The purpose of this method is to determines all the 12:00:00 days # within a start and end date @@ -194,7 +279,7 @@ def thumbnail_plot(dates, data, ymin, ymax, # @param create_air_dew_plot - boolean specifying if air temp and dewpoint # @param output - output filename pattern # should be in same plot -def create_thumbnail(frame, ymin, ymax, create_air_dew_plot, output): +def create_thumbnail(plot_names, frame, ymin, ymax, create_air_dew_plot, output): #see if we're going to need subplots #subplots needed when len is greater than 2 for one variable #or greater than 4 for two variables @@ -213,8 +298,7 @@ def create_thumbnail(frame, ymin, ymax, create_air_dew_plot, output): numberPlots = len(list(frame.columns.values)) / 2 plotNumber = numberPlots * 100 + 10 - stamps = list(frame.index) - + stamps = frame.index #get dewpoint data if create_air_dew_plot: all_data = frame['dewpoint'] @@ -222,9 +306,9 @@ def create_thumbnail(frame, ymin, ymax, create_air_dew_plot, output): good_list = get_good_data(qc_data, stamps, all_data) dewpoint_data = good_list[0] - dewpoint_stamps= good_list[1] + dewpoint_stamps = good_list[1] - for name in CHOICES: + for name in plot_names: if name == 'dewpoint' and create_air_dew_plot: continue @@ -257,18 +341,12 @@ def create_thumbnail(frame, ymin, ymax, create_air_dew_plot, output): thumbnail_plot(good_stamps, good_data, ymin[name], ymax[name], dewpoint_stamps, dewpoint_data, output, False) - plt.savefig(output + '.thumbnail.png') + plt.savefig(output.format(plot_name='test', start_time=good_stamps[0])) def full_plot_stamps(stamps): - first_stamp = np.sort(stamps)[0] - - first_stamp = first_stamp.replace(hour=0, minute=0, second=0) - - #print([((s - first_stamp).total_seconds()/3600) for s in stamps]) - - #print([(int((s - first_stamp).total_seconds()) / 3600) for s in stamps]) - - return [((s - first_stamp).total_seconds()/3600) for s in stamps] + # assume the timestamps are monotonic + first_stamp = stamps[0].replace(hour=0, minute=0, second=0) + return [((s - first_stamp).total_seconds()/3600) for s in stamps] # The purpose of this method is to change how the y ticks are shown # @param min - minimum of the y range right now @@ -277,8 +355,7 @@ def full_plot_stamps(stamps): def get_new_labels(mini, maxi): delta = math.ceil((maxi - mini) / 6) - - return np.arange(mini, (mini + delta*6), delta) + return np.arange(mini, (mini + delta*6), delta) # The purpose of this method is to create a full_size plot # @param frame - all data I need in a data frame @@ -292,7 +369,7 @@ def get_new_labels(mini, maxi): # @param wind_direction - says different yaxis labels # @param accum_precip - different yaxis handling # @param see if we need subplots -def full_plot(dates, data, ymin, ymax, +def full_plot(fig, axes, dates, data, ymin, ymax, dewpoint_stamps, dewpoint_data, output, wind_dir, accum_precip, need_subplots): @@ -318,15 +395,13 @@ def full_plot(dates, data, ymin, ymax, else: + # import ipdb; ipdb.set_trace() plt.plot(dates, data, 'k') - plt.axis([min(dates), max(dates), ymin, ymax]) + # plt.axis([min(dates), max(dates), ymin, ymax]) if not need_subplots: return - #get axes - axes = plt.gca() - #if not wind_direction, reset yaxis ticks if not wind_dir: if not accum_precip: @@ -358,323 +433,207 @@ def full_plot(dates, data, ymin, ymax, def get_subtitle_location(numSubPlots): return 1 - 0.05*numSubPlots -# The purpose of this method is to create a full size quicklook -# @param frame - all data I need in a data frame -# @param ymin - lower limit of the y axis if specified. set only when --daily is given -# @param ymax - upper limit of yaxis if specified. Set only when --daily is given -# @param create_air_dew_plot - boolean specifying if air temp and dewpoint -# should be in same plot -def create_full_plot(frame, ymin, ymax, create_air_dew_plot, output): - #see if we're going to need subplots - #subplots needed when len is greater than 2 for one variable - #or greater than 4 for two variables - - need_subplots = len(list(frame.columns.values)) > 2 and not create_air_dew_plot - need_subplots = need_subplots or len(list(frame.columns.values)) > 4 and create_air_dew_plot - - #get need supplot vars - if need_subplots: - plots_created = 1 - - if create_air_dew_plot: - numberPlots = (len(list(frame.columns.values)) - 2) / 2 - plotNumber = numberPlots * 100 + 10 - - else: - numberPlots = len(list(frame.columns.values)) / 2 - plotNumber = numberPlots * 100 + 10 - - stamps = list(frame.index) - - #get dewpoint data - if create_air_dew_plot: - all_data = frame['dewpoint'] - qc_data = frame['qc_dewpoint'] - - good_list = get_good_data(qc_data, stamps, all_data) - dewpoint_data = good_list[0] - dewpoint_stamps= good_list[1] - - for name in CHOICES: - #if dewpoint with temp or a name not in frame - # continue - if name == 'dewpoint' and create_air_dew_plot: - continue - - if name not in list(frame.columns.values): - continue - - all_data = frame[name] - qc_data = frame['qc_' + name] - - good_list = get_good_data(qc_data, stamps, all_data) - good_data = good_list[0] - good_stamps= good_list[1] - - dateString = good_stamps[0].strftime('%m/%d/%Y') - - # create plots - if need_subplots: - if plots_created == 1: - plt.figure().suptitle('AO&SS Building Tower Meteorogram ' + dateString, fontsize=13) - ax1 = plt.subplot(plotNumber + plots_created) - ax1.set_title(TITLES[name], x=0.5, y=get_subtitle_location(numberPlots), fontsize=8) - - else: - curr_plot = plt.subplot(plotNumber + plots_created, sharex=ax1) - curr_plot.set_title(TITLES[name], x=0.5, y=get_subtitle_location(numberPlots), fontsize=8) - - plots_created += 1 - - else: - plt.figure().suptitle('AO&SS Building Tower ' + IND_TITLES[name] + ' ' + dateString, fontsize=13) - plt.ylabel(TITLES[name]) - - #if we don't need two lines in same plot, plot - if not (create_air_dew_plot and name == 'air_temp'): - full_plot(good_stamps, good_data, ymin[name], ymax[name], - None, None, output, name == 'wind_dir', - name == 'accum_precip', need_subplots) - - else: - full_plot(good_stamps, good_data, ymin[name], ymax[name], - dewpoint_stamps, dewpoint_data, output, False, False, - need_subplots) - - if need_subplots: - plt.subplots_adjust(hspace=0, bottom=0.125) - - plt.xlabel('Time (UTC)') - - #reverse code done in create_full_plot - axes = plt.gca() - axes.get_xaxis().get_major_ticks()[-1].set_visible(True) - axes.get_xaxis().get_major_ticks()[0].set_visible(True) +def create_full_plot(plot_names, frame, output, start_time=None, end_time=None): + """ - plt.savefig(output + '.png') + Args: + plot_names: + frame: + output: + start_time: + end_time: + daily: Whether or not this plot should represent one day of data -# the purpose of this method is to use TW's algo to convert -# tempC and relhum to dewpoint -# @param tempC - temperature value in deg C -# @param relhum - relative humidity value in % + Returns: -def calcDewpoint(tempC, relhum): """ - Algorithm from Tom Whittaker tempC is the temperature in degrees Celsius, - relhum is the relative humidity as a percentage. - - :param tempC: temperature in celsius - :param relhum: relative humidity as a percentage - """ - if tempC is None or relhum is None: - return np.NaN - - gasconst = 461.5 - latheat = 2500800.0 - - dp = 1.0 / (1.0 / (273.15 + tempC) - gasconst * np.log((0.0 + relhum) / 100) / - (latheat - tempC * 2397.5)) - - return min(dp - 273.15, tempC) - -# The purpose of this method is to check to see if we have dewpoint data -# if we do not, fill in dewpoint data -# @param frame - pandas frame of nc data -# @return frame with dewpoint data and qc_dewpoint with only good data flagged - -def check_dewpoint(frame): - dewpoint = frame['dewpoint'] - qc_dewpoint = frame['qc_dewpoint'] - - good_list = get_good_data(qc_dewpoint.tolist(), list(frame.index), dewpoint.tolist()) - - # no dewpoint data that is viable - if len(good_list[0]) == 0: - temp = frame['air_temp'].tolist() - rel_hum = frame['rh'].tolist() - qc_temp = frame['qc_air_temp'].tolist() - qc_rel_hum = frame['qc_rh'].tolist() - stamps = list(frame.index) - - newDewpoint = {} - - qc_dew = {} - - for idx, rh_quality in enumerate(qc_rel_hum): - rh = rel_hum[idx] - temp_quality = qc_temp[idx] - air_temp = temp[idx] - stamp = stamps[idx] - - if not math.isnan(temp_quality): - air_temp = None - - elif not math.isnan(rh_quality): - rh = None + #see if we're going to need subplots + #subplots needed when len is greater than 2 for one variable + #or greater than 4 for two variables - dew = calcDewpoint(air_temp, rh) + # need_subplots = len(list(frame.columns.values)) > 2 and not create_air_dew_plot + # need_subplots = need_subplots or len(list(frame.columns.values)) > 4 and create_air_dew_plot - newDewpoint[stamp] = dew + #get need supplot vars + # if need_subplots: + # plots_created = 1 + # + # if create_air_dew_plot: + # numberPlots = (len(list(frame.columns.values)) - 2) / 2 + # plotNumber = numberPlots * 100 + 10 + # + # else: + # numberPlots = len(list(frame.columns.values)) / 2 + # plotNumber = numberPlots * 100 + 10 + + for name in plot_names: + plot_maker = PLOT_TYPES.get(name, PlotMaker(name, (name,))) + var_names = [] + for var_name in plot_maker.deps: + if var_name not in frame: + raise ValueError("Missing required variable '{}' for plot '{}'".format(var_name, name)) + var_names.append(var_name) + # write NaNs where QC values are not 0 + qc_name = 'qc_' + var_name + if qc_name in frame: + frame[var_name].mask(frame[qc_name] != 0) + var_names.append(qc_name) + + # create a frame that doesn't include any of the bad values + plot_frame = frame[var_names] + plot_frame = plot_frame[~plot_frame.isnull().any(axis=1)] + + fig = plt.figure() + ax = plot_maker.create_plot(plot_frame, fig) + ax.set_xlim(start_time, end_time) + import matplotlib.dates as md + xloc = md.AutoDateLocator(minticks=5, maxticks=8, interval_multiples=True) + xfmt = md.AutoDateFormatter(xloc) + def _fmt(interval, x, pos=None): + x_num = md.num2date(x).replace(tzinfo=None) + delta_seconds = (x_num - plot_frame.index[0].replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() + num_hours = delta_seconds / 3600. + if interval == md.HOURLY: + return "{:02.0f}".format(num_hours) + elif interval == md.MINUTELY: + num_minutes = delta_seconds / 60. + num_minutes -= int(num_hours) * 60. + return "{:02.0f}:{:02.0f}".format(int(num_hours), num_minutes) + else: + return x.strftime("{%Y-%m-%d}") + from functools import partial + xfmt.scaled[1. / md.MINUTES_PER_DAY] = plt.FuncFormatter(partial(_fmt, md.MINUTELY)) + xfmt.scaled[1. / md.HOURS_PER_DAY] = plt.FuncFormatter(partial(_fmt, md.HOURLY)) + ax.xaxis.set_major_locator(xloc) + ax.xaxis.set_major_formatter(xfmt) + + out_fn = output.format(plot_name=name, start_time=plot_frame.index[0].to_pydatetime()) + LOG.info("Saving plot '{}' to filename '{}'".format(name, out_fn)) + fig.savefig(out_fn) + + # all_data = frame[name] + # qc_data = frame['qc_' + name] + # + # good_list = get_good_data(qc_data, stamps, all_data) + # good_data = good_list[0] + # good_stamps = good_list[1] + # + # dateString = good_stamps[0].strftime('%m/%d/%Y') + # + # # create plots + # if need_subplots: + # if plots_created == 1: + # plt.figure().suptitle('AO&SS Building Tower Meteorogram ' + dateString, fontsize=13) + # ax1 = plt.subplot(plotNumber + plots_created) + # ax1.set_title(TITLES[name], x=0.5, y=get_subtitle_location(numberPlots), fontsize=8) + # else: + # curr_plot = plt.subplot(plotNumber + plots_created, sharex=ax1) + # curr_plot.set_title(TITLES[name], x=0.5, y=get_subtitle_location(numberPlots), fontsize=8) + # + # plots_created += 1 + # + # else: + # plt.figure().suptitle('AO&SS Building Tower ' + IND_TITLES[name] + ' ' + dateString, fontsize=13) + # plt.ylabel(TITLES[name]) + # + # #if we don't need two lines in same plot, plot + # if not (create_air_dew_plot and name == 'air_temp'): + # full_plot(good_stamps, good_data, ymin[name], ymax[name], + # None, None, output, name == 'wind_dir', + # name == 'accum_precip', need_subplots) + # + # else: + # full_plot(good_stamps, good_data, ymin[name], ymax[name], + # dewpoint_stamps, dewpoint_data, output, False, False, + # need_subplots) + + # if need_subplots: + # plt.subplots_adjust(hspace=0, bottom=0.125) + # + # plt.xlabel('Time (UTC)') - if math.isnan(dew): - qc_dew[stamp] = 1 - else: - qc_dew[stamp] = np.NaN - - newDewpoint = pd.Series(newDewpoint) - qc_dew = pd.Series(qc_dew) +def _dt_convert(datetime_str): + try: + return dt.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S') + except ValueError: + return dt.strptime(datetime_str, '%Y-%m-%d') - frame['dewpoint'] = newDewpoint - frame['qc_dewpoint'] = qc_dew - - return frame def main(): import argparse - - #argparse description parser = argparse.ArgumentParser(description="Use data from level_b1 netCDF files to create netCDF files") - - #argparse verbosity info - parser.add_argument('-v', '--verbose', action='count', + parser.add_argument('-v', '--verbose', action='count', default=int(os.environ.get("VERBOSITY", 2)), dest='verbosity', help=('each occurence increases verbosity 1 level through' + ' ERROR-WARNING-INFO-DEBUG (default INFO)')) - - #argparse start and end times parser.add_argument('-s', '--start-time', type=_dt_convert, help="Start time of plot. If only -s is given, a plot of " + "only that day is created. Formats allowed: \'YYYY-MM-DDTHH:MM:SS\', \'YYYY-MM-DD\'") - parser.add_argument('-e', '--end-time', type=_dt_convert, help="End time of plot. If only -e is given, a plot of only that day is " + "created. Formats allowed: \'YYYY-MM-DDTHH:MM:SS\', \'YYYY-MM-DD\'") - - #netcdf file paths - parser.add_argument("input_files", nargs="+", help="aoss_tower_level_b1 paths") - - #output filename pattern - parser.add_argument('-o', '--output', help="filename pattern") - - #thumbnail or full - parser.add_argument('-t', '--thumb-nail', action='store_true', help="if specified, script creates a thumbnail") - - #plot names - parser.add_argument('--varnames', nargs="+", choices=CHOICES, - required=True, - help="the variable names for the desired plot. Valid choices: air_temp, " + - "dewpoint, rh, pressure, wind_speed, wind_dir, accum_precip, solar_flux") - - #--daily flag - parser.add_argument('-d', '--daily', action='store_true', - help="creates a plot for every day. Usually used to create plots " + - "that will line up for aoss tower quicklooks page") - + parser.add_argument('--met-plots', nargs='+', + help="Override plots to use in the combined meteorogram plot") + parser.add_argument("input_files", nargs="+", help="aoss_tower_level_b1 files") + parser.add_argument('-o', '--output', default="{plot_name}_{start_time:%Y%m%d_%H%M%S}.png", help="filename pattern") + parser.add_argument('-t', '--thumbnail', action='store_true', help="if specified, script creates a thumbnail") + parser.add_argument('-p', '--plot-names', nargs="+", + required=True, + help="the variable names or plot types to create") + parser.add_argument('-d', '--daily', action='store_true', + help="creates a plot for every day. Usually used to create plots " + + "that will line up for aoss tower quicklooks page") args = parser.parse_args() levels = [logging.ERROR, logging.WARN, logging.INFO, logging.DEBUG] - level = levels[min(3, args.verbosity)] - logging.basicConfig(level=level) - var_names = args.varnames - - create_air_dew_plot = False - - #see if we need to plot air_temp and dewpoint on the same plot - if 'air_temp' in var_names and 'dewpoint' in var_names: - create_air_dew_plot = True - var_names.remove('dewpoint') + if not os.path.splitext(args.output)[-1]: + LOG.warning("File pattern provided does not have a file extension") - #get data - frame = get_data(args.input_files) + # check the dependencies for the meteorogram + if args.met_plots: + assert 'meteorogram' not in args.met_plots + PLOT_TYPES['meteorogram'].deps = args.met_plots - frame = check_dewpoint(frame) - - #only have the data we need - for name in CHOICES: - if name == 'dewpoint' and create_air_dew_plot: - continue - - if name not in var_names: - del frame[name] - del frame['qc_' + name] + plot_deps = [PLOT_TYPES[k].deps if k in PLOT_TYPES else (k,) for k in args.plot_names] + plot_deps = list(set(d for deps in plot_deps for d in deps)) + frame = get_data(args.input_files, plot_deps) + bad_plot_names = set(args.plot_names) - (set(frame.columns) | set(PLOT_TYPES.keys())) + if bad_plot_names: + raise ValueError("Unknown plot name(s): {}".format(", ".join(bad_plot_names))) #frame only contains data from start-end times - if(args.start_time and args.end_time): - start_filter = args.start_time.strftime('%Y-%m-%d %H:%M:%S') - end_filter = args.end_time.strftime('%Y-%m-%d %H:%M:%S') - frame = frame[start_filter: end_filter] - - #frame only contains data from start-end of that day - elif(args.start_time): + if args.start_time and args.end_time: + frame = frame[args.start_time: args.end_time] + elif args.start_time: + #frame only contains data from start-end of that day end_time = args.start_time.replace(hour=23, minute=59, second=59) - start_filter = args.start_time.strftime('%Y-%m-%d %H:%M:%S') - end_filter = end_time.strftime('%Y-%m-%d %H:%M:%S') - frame = frame[start_filter: end_filter] - - ymin = {} - ymax = {} + frame = frame[args.start_time: end_time] if not args.daily: - for name in CHOICES: - if name == 'dewpoint' and create_air_dew_plot: - continue - - if name not in frame: - continue - - ymin[name] = None - ymax[name] = None - - if(args.thumb_nail): - create_thumbnail(frame, ymin, ymax, create_air_dew_plot, args.output) - - else: - create_full_plot(frame, ymin, ymax, create_air_dew_plot, args.output) - + # allow plotting methods to write inplace on a copy + frames = [frame.copy()] else: - for name in CHOICES: - if name == 'dewpoint' and create_air_dew_plot: - continue - - if name not in frame: - continue - - if name == 'accum_precip': - ymin[name] = frame[name].min() - ymax[name] = frame[name].max() - - elif name == 'air_temp' and create_air_dew_plot: - ymin[name] = math.floor(frame[name].min()) - ymax[name] = math.ceil(frame[name].max()) + frames = (group[1] for group in frame.groupby(frame.index.day)) - dew_min = math.floor(frame['dewpoint'].min()) - dew_max = math.ceil(frame['dewpoint'].max()) - - ymin[name] = min(dew_min, ymin[name]) - ymax[name] = max(dew_max, ymax[name]) - - - else: - ymin[name] = math.floor(frame[name].min()) - ymax[name] = math.ceil(frame[name].max()) - - frameList = [(group[1]) for group in frame.groupby(frame.index.day)] - - counter = 0 + for frame in frames: + if args.daily: + # modify start and end time to the current day + start_time = frame.index[0].to_pydatetime().replace(hour=0, minute=0, second=0, microsecond=0) + end_time = frame.index[0].to_pydatetime().replace(hour=23, minute=59, second=59, microsecond=999999) + else: + start_time = args.start_time + end_time = args.end_time - for frame in frameList: - if args.thumb_nail: - create_thumbnail(frame, ymin, ymax, create_air_dew_plot, args.output.format(str(counter))) - else: - create_full_plot(frame, ymin, ymax, create_air_dew_plot, args.output.format(str(counter))) - counter += 1 - plt.gcf().clear() + if args.thumbnail: + create_thumbnail(args.plot_names, frame, args.output, start_time, end_time) + else: + create_full_plot(args.plot_names, frame, args.output, start_time, end_time) if __name__ == "__main__": - main() + sys.exit(main())