diff --git a/config_cam_baseline_example.yaml b/config_cam_baseline_example.yaml index 1dfc96bed..2293b9a97 100644 --- a/config_cam_baseline_example.yaml +++ b/config_cam_baseline_example.yaml @@ -65,10 +65,10 @@ user: 'USER-NAME-NOT-SET' diag_basic_info: #History file string to match (eg. cam.h0 or ocn.pop.h.ecosys.nday1) - # Only affects timeseries as everything else uses timeseries + # Only affects timeseries as everything else uses timeseries # Leave off trailing '.' #Default: cam.h0 - hist_str: cam.h0 + hist_str: cam.h0 #Is this a model vs observations comparison? #If "false" or missing, then a model-model comparison is assumed: @@ -151,6 +151,11 @@ diag_cam_climo: #Name of CAM case (or CAM run name): cam_case_name: b.e20.BHIST.f09_g17.20thC.297_05 + #Case nickname + #If left blank, it will default to cam_case_name + # ** NOTE: if nickname starts with a zero, the string must be in quotes! ** + case_nickname: ${diag_cam_climo.cam_case_name} + #Location of CAM history (h0) files: #Example test files cam_hist_loc: /glade/p/cesm/ADF/${diag_cam_climo.cam_case_name} @@ -201,6 +206,10 @@ diag_cam_climo: # - b.e20.BHIST.f09_g17.20thC.297_05 # - b1850.f19_g17.validation_mct.004 + #case_nickname: + # - nickname 1 + # - "026c" + #calc_cam_climo: # - true # - true @@ -259,6 +268,11 @@ diag_cam_baseline_climo: #Name of CAM baseline case: cam_case_name: b.e20.BHIST.f09_g16.20thC.125.02 + #Case nickname + #If left blank or if missing, nickname will default to baseline cam_case_name + # ** NOTE: if nickname starts with a zero, the string must be in quotes! ** + case_nickname: ${diag_cam_baseline_climo.cam_case_name} + #Location of CAM baseline history (h0) files: #Example test files cam_hist_loc: /glade/p/cesm/ADF/${diag_cam_baseline_climo.cam_case_name} @@ -377,6 +391,15 @@ diag_var_list: # +# Make mulit-plot comparison image for specified plots types +# This will only run if there are more than one specified test cases declared above +# NOTE: These plot types also need to be present above in the plotting_scripts section (for now) and variables in the diag_var_list +# NOTE: Only global LatLon multi-case plots are available at this time - more are coming. JR +multi_case_plots: + global_latlon_map: + - SST + - PSL + # Options for multi-case regional contour plots (./plotting/regional_map_multicase.py) # region_multicase: # region_spec: [slat, nlat, wlon, elon] diff --git a/lib/adf_diag.py b/lib/adf_diag.py index c3a5eded9..0e0327939 100644 --- a/lib/adf_diag.py +++ b/lib/adf_diag.py @@ -169,6 +169,9 @@ def __init__(self, config_file, debug=False): self.expand_references(self.__cvdp_info) #End if + #Add multi plots info to object: + self.__multi_case_plots = self.read_config_var('multi_case_plots') + #Add averaging script names: self.__time_averaging_scripts = self.read_config_var('time_averaging_scripts') @@ -197,6 +200,16 @@ def get_cvdp_info(self, var_str, required=False): conf_dict=self.__cvdp_info, required=required) + def get_multi_case_info(self, var_str, required=False): + """ + Return the config variable from 'multi_case_plots' as requested by + the user. + """ + + return self.read_config_var(var_str, + conf_dict=self.__multi_case_plots, + required=required) + ######### #Script-running functions ######### diff --git a/lib/adf_info.py b/lib/adf_info.py index 3b39e0cd5..0ade67b94 100644 --- a/lib/adf_info.py +++ b/lib/adf_info.py @@ -116,6 +116,16 @@ def __init__(self, config_file, debug=False): #Initialize "compare_obs" variable: self.__compare_obs = self.get_basic_info('compare_obs') + #Case names: + case_names = self.get_cam_info('cam_case_name', required=True) + + #Grab test case nickname(s) + test_nicknames = self.get_cam_info('case_nickname') + if test_nicknames is None: + #If no nicknames exist, then just set to case names + test_nicknames = case_names + #End if + #Check if a CAM vs AMWG obs comparison is being performed: if self.__compare_obs: @@ -125,6 +135,7 @@ def __init__(self, config_file, debug=False): #Also set data name for use below: data_name = "Obs" + base_nickname = "Obs" #Set the baseline years to empty strings: syear_baseline = "" @@ -154,20 +165,32 @@ def __init__(self, config_file, debug=False): base_climo_yrs = sorted(np.unique([i.stem[-7:-3] for i in files_list])) if syear_baseline is None: - print(f"No given start year for {data_name}, using first found year...") + print(f"\nNo given start year for {data_name}, using first found year...") syear_baseline = int(base_climo_yrs[0]) #End if if eyear_baseline is None: - print(f"No given end year for {data_name}, using last found year...") + print(f"No given end year for {data_name}, using last found year...\n") eyear_baseline = int(base_climo_yrs[-1]) #End if #End if + #Grab baseline nickname + base_nickname = self.get_baseline_info('case_nickname') + if base_nickname == None: + base_nickname = data_name + #End if + data_name += f"_{syear_baseline}_{eyear_baseline}" #End if + #Initialize case nicknames: + self.__test_nicknames = test_nicknames + self.__base_nickname = base_nickname + + #Initialize baseline climo years: self.__syear_baseline = syear_baseline self.__eyear_baseline = eyear_baseline + #Create plot location variable for potential use by the website generator. #Please note that this is also assumed to be the output location for the analyses scripts: #------------------------------------------------------------------------- @@ -176,9 +199,6 @@ def __init__(self, config_file, debug=False): #Plot directory: plot_dir = self.get_basic_info('cam_diag_plot_loc', required=True) - #Case names: - case_names = self.get_cam_info('cam_case_name', required=True) - #Start years (not currently required): syears = self.get_cam_info('start_year') @@ -200,7 +220,6 @@ def __init__(self, config_file, debug=False): for case_idx, case_name in enumerate(case_names): if syears[case_idx] is None or eyears[case_idx] is None: - print(f"No given climo years for {case_name}...") starting_location = Path(cam_hist_locs[case_idx]) files_list = sorted(starting_location.glob('*'+hist_str+'.*.nc')) case_climo_yrs = sorted(np.unique([i.stem[-7:-3] for i in files_list])) @@ -214,6 +233,7 @@ def __init__(self, config_file, debug=False): #End if #End if + #Append climo years to case directory case_name += f"_{syears[case_idx]}_{eyears[case_idx]}" #Set the final directory name and save it to plot_location: @@ -224,12 +244,28 @@ def __init__(self, config_file, debug=False): if case_idx == 0: first_case_dir = direc_name #End if - #End for + #Initialize case climo years: self.__syears = syears self.__eyears = eyears + #Make directoriess for multi-case diagnostics if applicable + if len(case_names) > 1: + multi_path = Path(self.get_basic_info('cam_diag_plot_loc', required=True)) + multi_path.mkdir(parents=True, exist_ok=True) + main_site_path = multi_path / "main_website" + main_site_path.mkdir(exist_ok=True) + main_site_assets_path = main_site_path / "assets" + main_site_assets_path.mkdir(exist_ok=True) + main_site_img_path = main_site_path / "html_img" + main_site_img_path.mkdir(exist_ok=True) + + #Initialize multi-case directories: + self.__main_site_path = main_site_path + self.__main_site_assets_path = main_site_assets_path + self.__main_site_img_path = main_site_img_path + #Finally add baseline case (if applicable) for use by the website table #generator. These files will be stored in the same location as the first #listed case. @@ -366,6 +402,25 @@ def climo_yrs(self): return {"syears":syears,"eyears":eyears, "syear_baseline":self.__syear_baseline, "eyear_baseline":self.__eyear_baseline} + # Create property needed to return the climo start (syear) and end (eyear) years to user: + @property + def case_nicknames(self): + """Return the test case and baseline nicknames to the user if requested.""" + #Note that a copy is needed in order to avoid having a script mistakenly + #modify this variable, as it is mutable and thus passed by reference: + test_nicknames = copy.copy(self.__test_nicknames) + + return {"test_nicknames":test_nicknames,"base_nickname":self.__base_nickname} + + # Create property needed to return the multi-case directories to scripts: + @property + def main_site_paths(self): + """Return the directories for multi-case diags if applicable.""" + + return {"main_site_path":self.__main_site_path, + "main_site_assets_path":self.__main_site_assets_path, + "main_site_img_path":self.__main_site_img_path} + ######### #Utility function to access expanded 'diag_basic_info' variables: diff --git a/lib/adf_web.py b/lib/adf_web.py index d15c2a8b3..e6e9ec064 100644 --- a/lib/adf_web.py +++ b/lib/adf_web.py @@ -56,6 +56,7 @@ class _WebData: """ def __init__(self, web_data, web_name, case_name, + plot_ext = None, category = None, season = None, plot_type = "Special", @@ -71,6 +72,7 @@ def __init__(self, web_data, web_name, case_name, self.category = category self.season = season self.plot_type = plot_type + self.plot_ext = plot_ext self.data_frame = data_frame self.html_file = html_file self.asset_path = asset_path @@ -180,6 +182,7 @@ def create_html(self): ######### def add_website_data(self, web_data, web_name, case_name, + plot_ext = None, category = None, season = None, plot_type = "Special", @@ -217,6 +220,7 @@ def add_website_data(self, web_data, web_name, case_name, #Initialize Pandas data frame logical: data_frame = False + html_file = [] #Check that the web_data is either a path #or a pandas dataframe: try: @@ -271,7 +275,12 @@ def add_website_data(self, web_data, web_name, case_name, #If multi-case, then save under the "multi-case" directory: if self.num_cases > 1: - html_file = self.__case_web_paths['multi-case']["table_pages_dir"] / html_name + multi_tbl_dir = self.__case_web_paths['multi-case']["table_pages_dir"] + #Avoid collecting individual case comparison tables + #Hacky - could probably use an update eventually - JR + if web_name != "case_comparison": + html_file.append(multi_tbl_dir/ html_name) + html_file.append(self.__case_web_paths[case_name]["table_pages_dir"] / html_name) else: html_file = self.__case_web_paths[case_name]["table_pages_dir"] / html_name #End if @@ -283,7 +292,7 @@ def add_website_data(self, web_data, web_name, case_name, #End if #Initialize web data object: - web_data = _WebData(web_data, web_name, case_name, + web_data = _WebData(web_data, web_name, case_name, plot_ext, category = category, season = season, plot_type = plot_type, @@ -300,6 +309,9 @@ def add_website_data(self, web_data, web_name, case_name, if plot_type not in self.__plot_type_multi: self.__plot_type_multi.append(plot_type) #End if + if plot_type not in self.__plot_type_order: + self.__plot_type_order.append(plot_type) + #End if else: #single case plot/ADF run if plot_type not in self.__plot_type_order: self.__plot_type_order.append(plot_type) @@ -332,58 +344,38 @@ def create_website(self): #If there is more than one non-baseline case, then create new website directory: if self.num_cases > 1: - main_site_path = Path(self.get_basic_info('cam_diag_plot_loc', required=True)) - main_site_path = main_site_path / "main_website" - main_site_path.mkdir(exist_ok=True) + #Grab all multi-case diagnostic directories + main_site_path = self.main_site_paths["main_site_path"] + main_site_assets_path = self.main_site_paths["main_site_assets_path"] + main_site_img_path = self.main_site_paths["main_site_img_path"] + case_sites = OrderedDict() + multi_layout = True else: main_site_path = "" #Set main_site_path to blank value + multi_layout = False #End if #Extract needed variables from yaml file: case_names = self.get_cam_info('cam_case_name', required=True) - #Time series files for unspecified climo years - cam_ts_locs = self.get_cam_info('cam_ts_loc', required=True) - - #Attempt to grab case start_years (not currently required): - ########################################################### - # This is for the header in the html files for climo yrs - #NOTE this may break when going to multi case... - syear_cases = self.get_cam_info('start_year') - eyear_cases = self.get_cam_info('end_year') + #Grab case climo years + syear_cases = self.climo_yrs["syears"] + eyear_cases = self.climo_yrs["eyears"] - if (syear_cases and eyear_cases) == None: - syear_cases = [None]*len(case_names) - eyear_cases = [None]*len(case_names) - - #Loop over model cases to catch all cases that have no climo years specified: - for case_idx, case_name in enumerate(case_names): - - if (syear_cases[case_idx] and eyear_cases[case_idx]) == None: - print(f"No given climo years for {case_name}...") - starting_location = Path(cam_ts_locs[case_idx]) - files_list = sorted(starting_location.glob('*nc')) - #This assumes CAM file names stay with this convention - #Better way to do this? - syear_cases[case_idx] = int(files_list[0].stem[-13:-9]) - eyear_cases[case_idx] = int(files_list[0].stem[-6:-2]) + #Grab baseline years (which may be empty strings if using Obs): + syear_baseline = self.climo_yrs["syear_baseline"] + eyear_baseline = self.climo_yrs["eyear_baseline"] #Set name of comparison data, which depends on "compare_obs": if self.compare_obs: data_name = "Obs" baseline_yrs = "" + syear_baseline = "" + eyear_baseline = "" else: data_name = self.get_baseline_info('cam_case_name', required=True) - #Attempt to grab baseline start_years (not currently required): - syear_baseline = self.get_baseline_info('start_year') - eyear_baseline = self.get_baseline_info('end_year') - - if (syear_baseline and eyear_baseline) == None: - syear_baseline = self.climo_yrs["syear_baseline"] - eyear_baseline = self.climo_yrs["eyear_baseline"] - #End if baseline_yrs=f"{syear_baseline} - {eyear_baseline}" #End if @@ -393,6 +385,33 @@ def create_website(self): #Extract variable defaults dictionary (for categories): var_defaults_dict = self.variable_defaults + #Extract requested multi-case multi-plots + multi_case_plots = self.read_config_var('multi_case_plots') + + if multi_case_plots: + #Grab all variables for each multi-case plot type + mvars = [] + #ext are plot type extensions (keys for multi-case plots) + #var_list should be a list of all vars for each plot map extensions + #var is iterative for all plot map extensions + for multi_var_list in [multi_case_plots[ext] for ext in multi_case_plots]: + for var in multi_var_list: + if ((self.compare_obs) and (var in self.var_obs_dict)) or (not self.compare_obs): + mvars.append(var) + + #Create multi-case site: + #Make a dictionary for plot type extensions for given plot type + #This can probably be populated in the for-loops during html creation... + #Or it should be declared somewhere higher up, like adf_info or something + multi_case_dict = {"global_latlon_map":"LatLon", + "zonal_mean":"Zonal", + "meridional":"Meridional", + "global_latlon_vect_map":"LatLon_Vector"} + + #Dictionary for multi-case website plot types + multi_plots = {"Tables": "html_table/mean_tables.html", + "Special":"html_img/multi_case_mean_diag_Special.html"} + #Set plot type html dictionary (for Jinja templating): plot_type_html = OrderedDict() for plot_type in self.__plot_type_order: @@ -412,13 +431,9 @@ def create_website(self): "mean_tables.html") else: multi_plot_type_html[plot_type] = os.path.join("html_img", - f"mean_diag_{plot_type}.html") + f"multi_case_mean_diag_{plot_type}.html") #End if #End for - else: - #Set to match standard plot type dict: - multi_plot_type_html = plot_type_html - #End if #Set main title for website: main_title = "CAM Diagnostics" @@ -443,9 +458,41 @@ def create_website(self): #so that we only had to do the web_data loop once, #but for now this will do. -JN mean_html_info = OrderedDict() + #Use this for multi-case diagnostic plots only + multi_mean_html_info = OrderedDict() + #Use this for multi-case with multi-plots + multi_plot_html_info = OrderedDict() #Create another dictionary needed for HTML pages that render tables: table_html_info = OrderedDict() + #Use this for multi-case diagnostic tables + multi_table_html_info = OrderedDict() + + #If this is a multi-case instance, then copy website to "main" directory: + if main_site_path: + self.__case_web_paths["multi-case"]['img_pages_dir'].mkdir(exist_ok=True) + + #Create CSS templates file path: + main_templates_path = main_site_path / "templates" + + #Also add path to case_sites dictionary: + #loop over cases: + for idx, case_name in enumerate(case_names): + #Check if case name is present in plot + if case_name in self.__case_web_paths: + #Add path to case_sites dictionary: + case_dir_ext = f"{case_name}_{syear_cases[idx]}_{eyear_cases[idx]}" + if self.compare_obs: + base_dir_ext = f"{data_name}" + else: + base_dir_ext = f"{data_name}_{syear_baseline}_{eyear_baseline}" + case_sites[case_name] = [os.path.join(os.curdir, + f"{case_dir_ext}_vs_{base_dir_ext}", + "index.html"),syear_cases[idx],eyear_cases[idx]] + + else: + #make empty list for non multi-case web generation + case_sites = [] #Loop over all web data objects: for web_data in self.__website_data: @@ -467,44 +514,46 @@ def create_website(self): shutil.copyfile(gif_file, css_files_dir / gif_file.name) #End for + #Check first for AMWG tables data frame if web_data.data_frame: #Create a directory that will hold table html files, if a table is present: if self.num_cases > 1: self.__case_web_paths['multi-case']['table_pages_dir'].mkdir(exist_ok=True) - else: - self.__case_web_paths[web_data.case]['table_pages_dir'].mkdir(exist_ok=True) #End if + self.__case_web_paths[web_data.case]['table_pages_dir'].mkdir(exist_ok=True) + #Add table HTML file to dictionary: #Note: Need to use data name instead of case name for tables. - table_html_info[web_data.name] = web_data.html_file.name - - if not web_data.data_frame: + if len(case_names) > 1: + if web_data.name != "case_comparison": + table_html_info[web_data.name] = web_data.html_file[0].name - #Create a directory that will hold just the html files for individual images: - self.__case_web_paths[web_data.case]['img_pages_dir'].mkdir(exist_ok=True) - - #Create a directory that will hold copies of the actual images: - self.__case_web_paths[web_data.case]['assets_dir'].mkdir(exist_ok=True) + multi_table_html_info[web_data.name] = web_data.html_file[0].name + else: + table_html_info[web_data.name] = web_data.html_file.name - #Move file to assets directory: - shutil.copy(web_data.data, web_data.asset_path) + #Now check all plot types + if not web_data.data_frame: + #Determine season value: + if web_data.season: + season = web_data.season + else: + season = "plot" #Just have the link be labeled "plot". + #End if #Extract plot_type: ptype = web_data.plot_type - #Initialize Ordered Dictionary for plot type: - if ptype not in mean_html_info: - mean_html_info[ptype] = OrderedDict() - #End if + #Extract web data name (usually the variable name): + var = web_data.name #Check if category has been provided for this web data: if web_data.category: #If so, then just use directly: category = web_data.category else: - #Check if variable in defaults dictionary: if web_data.name in var_defaults_dict: #If so, then extract category from dictionary: @@ -515,43 +564,96 @@ def create_website(self): #End if #End if - if category not in mean_html_info[ptype]: - mean_html_info[ptype][category] = OrderedDict() - #End if - - #Extract web data name (usually the variable name): - name = web_data.name + #Check to see if there are multiple-cases + if main_site_path: + #Check to see if the user has multi-plots enabled + if multi_case_plots: + #Loop over each variable in multi-case plot variables + #Check if plot ext is in requested multi-case plot types + if (web_data.plot_ext in multi_case_plots.keys()) and (var in mvars): + + #Initialize Ordered Dictionary for multi case plot type: + if ptype not in multi_plot_html_info: + multi_plot_html_info[ptype] = OrderedDict() + #End if + #Initialize Ordered Dictionary for category: + if category not in multi_plot_html_info[ptype]: + multi_plot_html_info[ptype][category] = OrderedDict() + #End if + if var not in multi_plot_html_info[ptype][category]: + multi_plot_html_info[ptype][category][var] = OrderedDict() + #End if + p = f"plot_page_multi_case_{var}_{season}_{ptype}_Mean.html" + if season not in multi_plot_html_info[ptype][category][var]: + multi_plot_html_info[ptype][category][var][season] = p + #End if + #End if (variable in multi-case plot variables) + #End if multi-case multi-plots + + #Need to isolate multi-case regular plots from the multi-case multi-plots + #QUESTION: Is there a better way? + if "multi_plot" not in str(web_data.html_file.name): + if ptype not in multi_mean_html_info: + multi_mean_html_info[ptype] = OrderedDict() + #End if + if category not in multi_mean_html_info[ptype]: + multi_mean_html_info[ptype][category] = OrderedDict() + #End if + if var not in multi_mean_html_info[ptype][category]: + multi_mean_html_info[ptype][category][var] = OrderedDict() + #End if + p = f"plot_page_multi_case_{var}_{season}_{ptype}_Mean.html" + if season not in multi_mean_html_info[ptype][category][var]: + multi_mean_html_info[ptype][category][var][season] = p + #End if + #End if (not multi-case multi-plots) + #End if (multi-case scenario) + + #Individual cases + #This will be used if multi-case diagnostics as well + #Create a directory that will hold just the html files for individual images: + self.__case_web_paths[web_data.case]['img_pages_dir'].mkdir(exist_ok=True) - #Initialize Ordered Dictionary for variable: - if name not in mean_html_info[ptype][category]: - mean_html_info[ptype][category][name] = OrderedDict() - #End if + #Create a directory that will hold copies of the actual images: + self.__case_web_paths[web_data.case]['assets_dir'].mkdir(exist_ok=True) - #Determine season value: - if web_data.season: - season = web_data.season - else: - season = "plot" #Just have the link be labeled "plot". - #End if + #Move file to assets directory: + try: + shutil.copy(web_data.data, web_data.asset_path) + except: + pass #Initialize Ordered Dictionary for season: - mean_html_info[ptype][category][name][season] = web_data.html_file.name - + #Need to ignore plots that may be generated for multi-case diags + #NOTE: There is probably a better way to do this - JR + if "multi_plot" not in str(web_data.html_file.name): + #Initialize Ordered Dictionary for plot type: + if ptype not in mean_html_info: + mean_html_info[ptype] = OrderedDict() + #End if + if category not in mean_html_info[ptype]: + mean_html_info[ptype][category] = OrderedDict() + #End if + #Initialize Ordered Dictionary for variable: + if var not in mean_html_info[ptype][category]: + mean_html_info[ptype][category][var] = OrderedDict() + #End if + mean_html_info[ptype][category][var][season] = web_data.html_file.name + #End if (check for multi_plot) #End if (data-frame check) #End for (web_data list loop) #Loop over all web data objects again: - for web_data in self.__website_data: - + #NOTE: This will be for non multi-case diagnostics + for idx,web_data in enumerate(self.__website_data): if web_data.data_frame: - #Create output HTML file path: if self.num_cases > 1: table_pages_dir = self.__case_web_paths['multi-case']['table_pages_dir'] - plot_types = multi_plot_type_html + table_pages_dir_indv = self.__case_web_paths[web_data.case]['table_pages_dir'] + else: table_pages_dir = self.__case_web_paths[web_data.case]['table_pages_dir'] - plot_types = plot_type_html #End if #Check if plot image already handles multiple cases, @@ -570,22 +672,46 @@ def create_website(self): float_format='{:6g}'.format) #Construct amwg_table.html + rend_kwarg_dict = {"title": main_title, + "case_name": case1, + "case_yrs": case_yrs, + "base_name": data_name, + "baseline_yrs": baseline_yrs, + "amwg_tables": table_html_info, + "table_name": web_data.name, + "table_html": table_html, + "multi_head": False, + "multi": multi_layout, + "case_sites": case_sites} + table_tmpl = jinenv.get_template('template_table.html') - table_rndr = table_tmpl.render(title=main_title, - case1=case1, - case2=data_name, - case_yrs=case_yrs, - baseline_yrs=baseline_yrs, - amwg_tables=table_html_info, - plot_types=plot_types, - table_name=web_data.name, - table_html=table_html - ) - - #Write mean diagnostic tables HTML file: - with open(web_data.html_file, 'w', encoding='utf-8') as ofil: - ofil.write(table_rndr) - #End with + + if main_site_path: + #Avoid single case comparison getting called here + #There might be a better way, but for now it works - JR + if web_data.name != "case_comparison": + rend_kwarg_dict["plot_types"] = multi_plot_type_html + rend_kwarg_dict["multi_head"] = "Table" + + table_rndr = table_tmpl.render(rend_kwarg_dict) + + #Write mean diagnostic tables HTML file: + html_file = web_data.html_file[0] + with open(html_file, 'w', encoding='utf-8') as ofil: + ofil.write(table_rndr) + + else: + rend_kwarg_dict["plot_types"] = plot_type_html + if web_data.case == data_name: + rend_kwarg_dict["case_name"] = case_names[0] + + table_rndr = table_tmpl.render(rend_kwarg_dict) + + #Write mean diagnostic tables HTML file: + html_file = web_data.html_file + with open(html_file, 'w', encoding='utf-8') as ofil: + ofil.write(table_rndr) + #Check if the mean plot type page exists for this case (or for multi-case): mean_table_file = table_pages_dir / "mean_tables.html" @@ -593,102 +719,98 @@ def create_website(self): #Construct mean_table.html mean_table_tmpl = jinenv.get_template('template_mean_tables.html') - mean_table_rndr = mean_table_tmpl.render(title=main_title, - case1=case1, - case2=data_name, - case_yrs=case_yrs, - baseline_yrs=baseline_yrs, - amwg_tables=table_html_info, - plot_types=plot_types, - ) + + #Reuse the rend_kwarg_dict, but ignore certain keys + #since all others are the same + new_dict = {k: rend_kwarg_dict[k] for k in rend_kwarg_dict.keys() - {'table_name', 'table_html'}} + + if main_site_path: + plot_types = multi_plot_type_html + new_dict["multi_head"] = "Table" + else: + plot_types = plot_type_html + + mean_table_rndr = mean_table_tmpl.render(new_dict) #Write mean diagnostic tables HTML file: with open(mean_table_file, 'w', encoding='utf-8') as ofil: ofil.write(mean_table_rndr) #End with #End if + #End if (tables) else: #Plot image + plot_types = plot_type_html - #Create output HTML file path: - img_pages_dir = self.__case_web_paths[web_data.case]['img_pages_dir'] - img_data = [os.path.relpath(web_data.asset_path, start=img_pages_dir), - web_data.asset_path.stem] + #Ensure that these will ignore any multi-case pages if they exist + if "main_website" not in str(web_data.html_file): + #Create output HTML file path: + img_pages_dir = self.__case_web_paths[web_data.case]['img_pages_dir'] - #Check if plot image already handles multiple cases: - if web_data.multi_case: - case1 = "Listed in plots." - plot_types = multi_plot_type_html - else: - case1 = web_data.case - plot_types = plot_type_html - #End if + img_data = [os.path.relpath(web_data.asset_path, start=img_pages_dir), + web_data.asset_path.stem] - tmpl = jinenv.get_template('template.html') #Set template - rndr = tmpl.render(title=main_title, - var_title=web_data.name, - season_title=web_data.season, - plottype_title=web_data.plot_type, - imgs=img_data, - case1=case1, - case2=data_name, - case_yrs=case_yrs, - baseline_yrs=baseline_yrs, - mydata=mean_html_info[web_data.plot_type], - plot_types=plot_types) #The template rendered - - #Write HTML file: - with open(web_data.html_file, 'w', encoding='utf-8') as ofil: - ofil.write(rndr) - #End with - - #Check if the mean plot type page exists for this case: - mean_ptype_file = img_pages_dir / f"mean_diag_{web_data.plot_type}.html" - if not mean_ptype_file.exists(): - - #Construct individual plot type mean_diag html files, if they don't - #already exist: - mean_tmpl = jinenv.get_template('template_mean_diag.html') - mean_rndr = mean_tmpl.render(title=main_title, - case1=case1, - case2=data_name, - case_yrs=case_yrs, - baseline_yrs=baseline_yrs, - mydata=mean_html_info[web_data.plot_type], - curr_type=web_data.plot_type, - plot_types=plot_types) - - #Write mean diagnostic plots HTML file: - with open(mean_ptype_file,'w', encoding='utf-8') as ofil: - ofil.write(mean_rndr) + rend_kwarg_dict = {"title": main_title, + "var_title": web_data.name, + "season_title": web_data.season, + "case_name": web_data.case, + "case_yrs": case_yrs, + "base_name": data_name, + "baseline_yrs": baseline_yrs, + "plottype_title": web_data.plot_type, + "imgs": img_data, + "mydata": mean_html_info[web_data.plot_type], + "plot_types": plot_types, + "multi": multi_layout} + + tmpl = jinenv.get_template('template.html') #Set template + + rndr = tmpl.render(rend_kwarg_dict) #The template rendered + + #Write HTML file: + with open(web_data.html_file, 'w', encoding='utf-8') as ofil: + ofil.write(rndr) #End with - #End if (mean_ptype exists) - - #Check if the mean plot type and var page exists for this case: - mean_ptype_plot_page = img_pages_dir / f"plot_page_{web_data.name}_{web_data.plot_type}.html" - if not mean_ptype_plot_page.exists(): - - #Construct individual plot type mean_diag html files, if they don't - #already exist: - plot_page_tmpl = jinenv.get_template('template_var.html') - plot_page_rndr = plot_page_tmpl.render(title=main_title, - var_title=web_data.name, - season_title=web_data.season, - plottype_title=web_data.plot_type, - case1=case1, - case2=data_name, - case_yrs=case_yrs, - baseline_yrs=baseline_yrs, - mydata=mean_html_info[web_data.plot_type], - curr_type=web_data.plot_type, - plot_types=plot_types) - - #Write mean diagnostic plots HTML file: - with open(mean_ptype_plot_page,'w', encoding='utf-8') as ofil: - ofil.write(plot_page_rndr) - #End with - #End if (data frame) + #Check if the mean plot type page exists for this case: + mean_ptype_file = img_pages_dir / f"mean_diag_{web_data.plot_type}.html" + if not mean_ptype_file.exists(): + + #Construct individual plot type mean_diag html files, if they don't + #already exist: + mean_tmpl = jinenv.get_template('template_mean_diag.html') + + #Remove keys from main dictionary for this html page + templ_rend_kwarg_dict = {k: rend_kwarg_dict[k] for k in rend_kwarg_dict.keys() - {'imgs', 'var_title', 'season_title'}} + + mean_rndr = mean_tmpl.render(templ_rend_kwarg_dict) + + #Write mean diagnostic plots HTML file: + with open(mean_ptype_file,'w', encoding='utf-8') as ofil: + ofil.write(mean_rndr) + #End with + #End if (mean_ptype exists) + + #Check if the mean plot type and var page exists for this case: + plot_page = f"plot_page_{web_data.name}_{web_data.plot_type}.html" + mean_ptype_plot_page = img_pages_dir / plot_page + if not mean_ptype_plot_page.exists(): + + #Construct individual plot type mean_diag html files, if they don't + #already exist: + plot_page_tmpl = jinenv.get_template('template_var.html') + + #Remove key from main dictionary for this html page + templ_var_rend_kwarg_dict = {k: rend_kwarg_dict[k] for k in rend_kwarg_dict.keys() - {'imgs'}} + + plot_page_rndr = plot_page_tmpl.render(templ_var_rend_kwarg_dict) + + #Write mean diagnostic plots HTML file: + with open(mean_ptype_plot_page,'w', encoding='utf-8') as ofil: + ofil.write(plot_page_rndr) + #End with + #End if (mean_ptype_plot_page) + #End if (check for multi-case diags) #Also check if index page exists for this case: index_html_file = \ self.__case_web_paths[web_data.case]['website_dir'] / "index.html" @@ -698,71 +820,388 @@ def create_website(self): plot_types = multi_plot_type_html else: plot_types = plot_type_html + plot_types = plot_type_html #End if #Construct index.html index_title = "AMP Diagnostics Prototype" index_tmpl = jinenv.get_template('template_index.html') + index_rndr = index_tmpl.render(title=index_title, - case1=web_data.case, - case2=data_name, + case_name=web_data.case, + base_name=data_name, case_yrs=case_yrs, baseline_yrs=baseline_yrs, - plot_types=plot_types) + plot_types=plot_types, + multi=multi_layout) #Write Mean diagnostics index HTML file: with open(index_html_file, 'w', encoding='utf-8') as ofil: ofil.write(index_rndr) #End with + #End if (plot images) #End for (web data loop) - #If this is a multi-case instance, then copy website to "main" directory: + # - - - - - - - - - - - - - - - - - - - - - - + # --- End single-case diagnostics --- + # - - - - - - - - - - - - - - - - - - - - - - + + + + # - - - - - - - - - - - - - - - - - - - - - - + # --- Checking for multi-case diagnostics --- + # - - - - - - - - - - - - - - - - - - - - - - + if main_site_path: - #Add "multi-case" to start of case_names: - case_names.insert(0, "multi-case") + #Loop over all web data objects AGAIN: + for web_data in self.__website_data: + + #Create CSS templates file path: + main_templates_path = main_site_path / "templates" + + #loop over all cases and make website directories: + for idx, case_name in enumerate(case_names): + #Check if case name is present in plot + if case_name in self.__case_web_paths: + #Extract website directory: + website_dir = self.__case_web_paths[case_name]['website_dir'] + if not website_dir.is_dir(): + website_dir.mkdir(parents=True) + #Copy website directory to "main site" directory: + shutil.copytree(website_dir, main_site_path / case_name) + + + #Starting multi-case tables if requested + # - - - - - - - - - - - - - - - - - - - + if web_data.data_frame: + + #Create all individual tables for the individual websites + + #Grab single case table path + table_pages_dir_indv = self.__case_web_paths[web_data.case]['table_pages_dir'] + + #Check if the mean plot type page exists for this case (or for multi-case): + mean_table_file = table_pages_dir_indv / "mean_tables.html" + table_keys = [web_data.case,data_name,"case_comparison"] + + table_dict = {} + for key in table_keys: + if self.compare_obs: + if (key != "Obs") and (key != "case_comparison"): + table_dict[key] = multi_table_html_info[key] + else: + table_dict[key] = multi_table_html_info[key] + #End for + + #Construct amwg_table.html + rend_kwarg_dict = {"title": main_title, "case_name": web_data.case, + "case_yrs": case_yrs, + "base_name": data_name, + "baseline_yrs": baseline_yrs, + "amwg_tables": table_dict, + "table_name": web_data.name, + "plot_types": plot_type_html, + "multi_head": True, + "multi": False, + "case_sites": case_sites} + + if not mean_table_file.exists(): + #Construct mean_table.html + mean_table_tmpl = jinenv.get_template('template_mean_tables.html') + + #Remove key from main dictionary for this html page + tmpl_rend_kwarg_dict = {k: rend_kwarg_dict[k] for k in rend_kwarg_dict.keys() - {'table_name'}} + + mean_table_rndr = mean_table_tmpl.render(tmpl_rend_kwarg_dict) + + #Write mean diagnostic tables HTML file: + with open(mean_table_file, 'w', encoding='utf-8') as ofil: + ofil.write(mean_table_rndr) + #End with + #End if - #Create CSS templates file path: - main_templates_path = main_site_path / "templates" + #Loop through all test cases (exclude baseline) + if web_data.case != data_name: + table_html = web_data.data.to_html(index=False, border=1, justify='center', + float_format='{:6g}'.format) - #loop over cases: - for case_name in case_names: + indv_html = table_pages_dir_indv / f"amwg_table_{web_data.name}.html" - #Check if case name is present in plot - if case_name in self.__case_web_paths: - #Extract website directory: - website_dir = self.__case_web_paths[case_name]['website_dir'] + if not indv_html.exists(): + table_tmpl = jinenv.get_template('template_table.html') + + tmpl_rend_kwarg_dict = rend_kwarg_dict + rend_kwarg_dict["table_html"] = table_html + + table_rndr = table_tmpl.render(rend_kwarg_dict) + + #Write mean diagnostic tables HTML file: + with open(indv_html, 'w', encoding='utf-8') as ofil: + ofil.write(table_rndr) + #end if (indv_html) + + #Baseline case added to all test case directories + # - this block should only run once when web_data is the baseline case + else: + table_html = web_data.data.to_html(index=False, border=1, justify='center', + float_format='{:6g}'.format) - #Copy website directory to "main site" directory: - shutil.copytree(website_dir, main_site_path / case_name) + rend_kwarg_dict["table_html"] = table_html - #Also add path to case_sites dictionary: - case_sites[case_name] = os.path.join(os.curdir, case_name, "index.html") + for case_name in case_names: + table_pages_dir_sp = self.__case_web_paths[case_name]['table_pages_dir'] + table_key = [case_name,data_name,"case_comparison"] - #Also make sure CSS template files have been copied over: - if not main_templates_path.is_dir(): - css_files_dir = self.__case_web_paths[case_name]['css_files_dir'] - shutil.copytree(css_files_dir, main_site_path / "templates") + base_table_dict = {key: multi_table_html_info[key] for key in table_key} + + rend_kwarg_dict["case_name"] = case_name + rend_kwarg_dict["amwg_tables"] = base_table_dict + + sp_html = table_pages_dir_sp / f"amwg_table_{data_name}.html" + if not sp_html.exists(): + + table_tmpl = jinenv.get_template('template_table.html') + + table_rndr = table_tmpl.render(rend_kwarg_dict) + + with open(sp_html, 'w', encoding='utf-8') as ofil: + ofil.write(table_rndr) + #End if (sp_html) + #End for (case_names) + #End if (baseline_name) + + + #Starting multi-case plots if requested + # - - - - - - - - - - - - - - - - - - - + if not web_data.data_frame: + + #Extract plot details + season = web_data.season + ptype = web_data.plot_type + var = web_data.name + ext = web_data.plot_ext + + #Check if category has been provided for this web data: + if web_data.category: + #If so, then just use directly: + category = web_data.category + else: + #Check if variable in defaults dictionary: + if web_data.name in var_defaults_dict: + #If so, then extract category from dictionary: + category = var_defaults_dict[web_data.name].get("category", + "No category yet") + else: + category = 'No category yet' + #End if #End if - #End if - #End for (model case loop) - #Create multi-case site: + #Check for multi-case multi-plots + if multi_case_plots: + #This currently runs web_data.case for every case, but in reality + #it really only needs to run once since the plots are + #already made with all cases. + #So just grab the first test case: + case1 = self.get_cam_info('cam_case_name', required=True)[0] + if str(web_data.case) == str(case1): + #Check if variable is in desired multi-case plot + #and if plot_type is in given multi-case plot set: + if (var in mvars) and (ext in multi_case_plots): + #Move file to assets directory: + if not web_data.data.is_file(): + shutil.copy(web_data.data, web_data.asset_path) + + #Create output HTML file path: + img_pages_dir = self.__case_web_paths["multi-case"]['img_pages_dir'] + multi_plot_page = f"{var}_{season}_{ptype}_multi_plot.png" + img_data = [os.path.relpath(main_site_assets_path / multi_plot_page, + start=main_site_img_path), + multi_plot_page] + + rend_kwarg_dict = {"title": main_title, + "var_title": var, + "season_title": season, + "case_yrs": case_yrs, + "base_name": data_name, + "baseline_yrs": baseline_yrs, + "plottype_title": ptype, + "imgs": img_data, + "mydata": multi_plot_html_info[ptype], + "plot_types": multi_plot_type_html, + "multi": multi_layout, + "case_sites": case_sites} + + multimean = f"plot_page_multi_case_{var}_{season}_{ptype}_Mean.html" + if not (img_pages_dir / multimean).exists(): + + tmpl = jinenv.get_template('template_multi_case.html') + + rndr = tmpl.render(rend_kwarg_dict) + + #Write HTML file: + with open(img_pages_dir / multimean, + 'w', encoding='utf-8') as ofil: + ofil.write(rndr) + #End if (multimean) + + #Check if the mean plot type and var page exists for this case: + img_pages_dir = self.__case_web_paths["multi-case"]['img_pages_dir'] + plot_page = f"plot_page_multi_case_{var}_{ptype}.html" + mean_ptype_plot_page = img_pages_dir / plot_page + + if not mean_ptype_plot_page.exists(): + + #Remove key from main dictionary for this html page + templ_var_rend_kwarg_dict = {k: rend_kwarg_dict[k] for k in rend_kwarg_dict.keys() - {'imgs'}} + + #Construct individual plot type mean_diag + #html files, if they don't already exist: + page_tmpl = jinenv.get_template('template_multi_case_var.html') + + plot_page_rndr = page_tmpl.render(templ_var_rend_kwarg_dict) + + #Write mean diagnostic plots HTML file: + with open(mean_ptype_plot_page,'w', encoding='utf-8') as ofil: + ofil.write(plot_page_rndr) + #End with + #End if (mean_ptype_plot_page exists) + + multi_mean = f"multi_case_mean_diag_{ptype}.html" + mean_ptype_file = main_site_img_path / multi_mean + if not mean_ptype_file.exists(): + + #Remove keys from main dictionary for this html page + templ_rend_kwarg_dict = {k: rend_kwarg_dict[k] for k in rend_kwarg_dict.keys() - {'imgs', 'var_title', 'season_title'}} + + #Construct individual plot type mean_diag + #html files, if they don't already exist: + tmp = jinenv.get_template('template_multi_case_mean_diag.html') + + mean_rndr = tmp.render(templ_rend_kwarg_dict) + + #Write mean diagnostic plots HTML file: + with open(mean_ptype_file,'w', encoding='utf-8') as ofil: + ofil.write(mean_rndr) + #End with + #End if (mean_ptype exists) + + #Loop over any non multi-case multi-plot scenarios + #ie multi-case Taylor Diagrams and multi-case QBO + if ext not in multi_case_dict: + #Move file to assets directory: + if not web_data.data.is_file(): + shutil.copy(web_data.data, web_data.asset_path) + + #Create output HTML file path: + img_pages_dir = self.__case_web_paths["multi-case"]['img_pages_dir'] + multi_plot_page = f"{var}_{season}_{ptype}_multi_plot.png" + img_data = [os.path.relpath(main_site_assets_path / multi_plot_page, + start=main_site_img_path), + multi_plot_page] + + rend_kwarg_dict = {"title": main_title, + "var_title": var, + "season_title": season, + "case_yrs": case_yrs, + "base_name": data_name, + "baseline_yrs": baseline_yrs, + "plottype_title": ptype, + "imgs": img_data, + "mydata": multi_mean_html_info[ptype], + "plot_types": multi_plot_type_html, + "multi": multi_layout, + "case_sites": case_sites} + + multimean = f"plot_page_multi_case_{var}_{season}_{ptype}_Mean.html" + if not (img_pages_dir / multimean).exists(): + tmpl = jinenv.get_template('template_multi_case.html') + + rndr = tmpl.render(rend_kwarg_dict) + + #Write HTML file: + with open(img_pages_dir / multimean, + 'w', encoding='utf-8') as ofil: + ofil.write(rndr) + #End if (multimean) + + #Check if the mean plot type and var page exists for this case: + img_pages_dir = self.__case_web_paths["multi-case"]['img_pages_dir'] + plot_page = f"plot_page_multi_case_{var}_{ptype}.html" + mean_ptype_plot_page = img_pages_dir / plot_page + + if not mean_ptype_plot_page.exists(): + + #Remove key from main dictionary for this html page + templ_var_rend_kwarg_dict = {k: rend_kwarg_dict[k] for k in rend_kwarg_dict.keys() - {'imgs'}} + + #Construct individual plot type mean_diag + #html files, if they don't already exist: + page_tmpl = jinenv.get_template('template_multi_case_var.html') + + plot_page_rndr = page_tmpl.render(templ_var_rend_kwarg_dict) + + #Write mean diagnostic plots HTML file: + with open(mean_ptype_plot_page,'w', encoding='utf-8') as ofil: + ofil.write(plot_page_rndr) + #End with + #End if (mean_ptype_plot_page exists) + + multi_mean = f"multi_case_mean_diag_{ptype}.html" + mean_ptype_file = main_site_img_path / multi_mean + if not mean_ptype_file.exists(): + + #Remove keys from main dictionary for this html page + templ_rend_kwarg_dict = {k: rend_kwarg_dict[k] for k in rend_kwarg_dict.keys() - {'imgs', 'var_title', 'season_title'}} + + #Construct individual plot type mean_diag + #html files, if they don't already exist: + tmp = jinenv.get_template('template_multi_case_mean_diag.html') + + mean_rndr = tmp.render(templ_rend_kwarg_dict) + + #Write mean diagnostic plots HTML file: + with open(mean_ptype_file,'w', encoding='utf-8') as ofil: + ofil.write(mean_rndr) + #End with + #End if (mean_ptype exists) + #End if (ext not in multi_case_dict) + #End if (web_data.data_frame) + #End for (web_data) + + #Also make sure CSS template files have been copied over: + if not main_templates_path.is_dir(): + #If not, just grab the files from the first test case directory + css_files_dir = self.__case_web_paths[case_names[-1]]['css_files_dir'] + shutil.copytree(css_files_dir, main_templates_path) + #End if + + if multi_case_plots: + for key in multi_case_plots: + #Update the dictionary to add any plot types specified in the yaml file + mcase_plot = f"html_img/multi_case_mean_diag_{multi_case_dict[key]}.html" + multi_plots[multi_case_dict[key]] = mcase_plot + #End for + #End if + main_title = "ADF Diagnostics" main_tmpl = jinenv.get_template('template_multi_case_index.html') main_rndr = main_tmpl.render(title=main_title, - case_sites=case_sites, - ) + case_sites=case_sites, + base_name=data_name, + baseline_yrs=baseline_yrs, + multi_plots=multi_plots) #Write multi-case main HTML file: outputfile = main_site_path / "index.html" with open(outputfile, 'w', encoding='utf-8') as ofil: ofil.write(main_rndr) #End with - #End if + #End if (multi case) #Notify user that script has finishedd: print(" ...Webpages have been generated successfully.") + #++++++++++++++++++++ #End Class definition #++++++++++++++++++++ diff --git a/lib/plotting_functions.py b/lib/plotting_functions.py index 99a373009..a1894eb57 100644 --- a/lib/plotting_functions.py +++ b/lib/plotting_functions.py @@ -8,6 +8,7 @@ #import statements: from typing import Optional +from pathlib import Path import numpy as np import xarray as xr import matplotlib as mpl @@ -1659,5 +1660,159 @@ def square_contour_difference(fld1, fld2, **kwargs): cb2 = fig.colorbar(img3, cax=cbax_bot, orientation='horizontal') return fig +##### + +############################### +# Multi-Case Multi-Plot Section +############################### + +def multi_latlon_plots(wks, ptype, case_names, nicknames, multi_dict, web_category, adfobj): + """ This is a multi-case comparison of test minus baseline for each test case: + wks: path for saved image. + Should be assets directory inside of the main_website directory + + ptype: ADF shortname for plot type. + For this plot it will be either LatLon or LatLon_Vector + + case_names: list of test case names only + + nicknames: list of test case nicknames + First entry of list will be list of test case nicknames + Second entry will be string object of baseline nickname + + multi_dict: ordered dictionary of difference data for each var, test case, and season + multi_dict[var][case_name][s] + + web_category: + variable category + + adfobj: ADF object + Needed to test if redo_plot is called in config yaml file + """ + + + nplots = len(nicknames[0]) + if nplots > 2: + ncols = 3 + else: + ncols = 2 + #End if + + #Check redo_plot. If set to True: remove old plot, if it already exists: + redo_plot = adfobj.get_basic_info('redo_plot') + + #Determine needed matplotlib normalization function: + normfunc,_ = use_this_norm() + + #Try and format spacing based on number of cases + # NOTE: ** this will have to change if figsize or dpi change ** + if nplots < 4: + hspace = -1.0 + else: + hspace = -0.85 + #End if + + nrows = int(np.ceil(nplots/ncols)) + if nrows == 1: + y_title = 0.265 + else: + y_title = 0.315 + #End if + + if nrows < 2: + nrows = 2 + #End if + + # specify the central longitude for the plot + central_longitude = get_central_longitude(adfobj) + proj = ccrs.PlateCarree(central_longitude=central_longitude) + # formatting for tick labels + lon_formatter = LongitudeFormatter(number_format='0.0f', + degree_symbol='', + dateline_direction_label=False) + lat_formatter = LatitudeFormatter(number_format='0.0f', + degree_symbol='') + for var in multi_dict.keys(): + if ((adfobj.compare_obs) and (var in adfobj.var_obs_dict)) or (not adfobj.compare_obs): + for case in multi_dict[var].keys(): + for season in multi_dict[var][case].keys(): + file_name = f"{var}_{season}_{ptype}_multi_plot.png" + if (not redo_plot) and Path(wks / file_name).is_file(): + #Continue to next iteration: + continue + elif (redo_plot) or (not Path(wks / file_name).is_file()): + fig_width = 15 + fig_height = 15+(3*nrows) #try and dynamically create size of fig based off number of cases (therefore rows) + fig, axs = plt.subplots(nrows=nrows,ncols=ncols,figsize=(fig_width,fig_height), facecolor='w', edgecolor='k', + sharex=True, + sharey=True, + subplot_kw={"projection": proj}) + + #Set figure title + plt.suptitle(f'All Case Comparison (Test - Baseline) {var}: {season}\n', fontsize=16, y=y_title)#y=0.325 y=0.225 + + count = 0 + img = [] + titles = [] + for r in range(0,nrows): + for c in range(0,ncols): + if count < nplots: + mdlfld = multi_dict[var][case_names[count]][season]["diff_data"] + lat = mdlfld['lat'] + mwrap, lon = add_cyclic_point(mdlfld, coord=mdlfld['lon']) + + # mesh for plots: + lons, lats = np.meshgrid(lon, lat) + + levelsdiff = multi_dict[var][case_names[count]][season]["vres"]["diff_contour_range"] + levelsdiff = np.arange(levelsdiff[0],levelsdiff[1]+levelsdiff[-1],levelsdiff[-1]) + + # color normalization for difference + if (np.min(levelsdiff) < 0) and (0 < np.max(levelsdiff)): + normdiff = normfunc(vmin=np.min(levelsdiff), vmax=np.max(levelsdiff), vcenter=0.0) + else: + normdiff = mpl.colors.Normalize(vmin=np.min(levelsdiff), vmax=np.max(levelsdiff)) + + cmap = multi_dict[var][case_names[count]][season]["vres"]['diff_colormap'] + + img.append(axs[r,c].contourf(lons, lats, mwrap, levels=levelsdiff, + cmap=cmap, norm=normdiff, + transform=proj)) + + #Set individual plot titles (case name/nickname) + titles.append(axs[r,c].set_title("$\mathbf{Test}:$"+f" {nicknames[0][count]}",loc='left',fontsize=8)) + titles.append(axs[r,c].set_title("$\mathbf{Baseline}:$"+f" {nicknames[1]}",loc='right',fontsize=8)) + + axs[r,c].spines['geo'].set_linewidth(1.5) #cartopy's recommended method + axs[r,c].coastlines() + axs[r,c].set_xticks(np.linspace(-180, 120, 6), crs=proj) + axs[r,c].set_yticks(np.linspace(-90, 90, 7), crs=proj) + axs[r,c].tick_params('both', length=5, width=1.5, which='major') + axs[r,c].tick_params('both', length=5, width=1.5, which='minor') + axs[r,c].xaxis.set_major_formatter(lon_formatter) + axs[r,c].yaxis.set_major_formatter(lat_formatter) + + else: + #Clear left over subplots if they don't fill the row x column matrix + axs[r,c].set_visible(False) + count = count + 1 + + # __COLORBARS__ + fig.colorbar(img[-1], ax=axs.ravel().tolist(), orientation='horizontal', + aspect=20, shrink=.5, location="bottom", + anchor=(0.5,-0.3), extend='both') + + #Clean up the spacing a bit + plt.subplots_adjust(wspace=0.3, hspace=hspace) + + fig.savefig(wks / file_name, bbox_inches='tight', dpi=300) + + adfobj.add_website_data(wks / file_name, file_name, case_names[0], plot_ext="global_latlon_map", + category=web_category, season=season, plot_type="LatLon",multi_case=True) + + #Close plots: + plt.close() + + ##################### #END HELPER FUNCTIONS diff --git a/lib/website_templates/template.html b/lib/website_templates/template.html index cfa488ab2..be92a6d7b 100644 --- a/lib/website_templates/template.html +++ b/lib/website_templates/template.html @@ -1,9 +1,7 @@ - - - ADF {{var_title}} + ADF {{ var_title }} @@ -14,6 +12,9 @@
-

Test Case:

{{ case1 }}

- years: {{ case_yrs }}


-

Baseline Case:

{{ case2 }}

- years: {{ baseline_yrs }}

+

Test Case:

{{ case_name }}

- years: {{ case_yrs }}


+

Baseline Case:

{{ base_name }}

- years: {{ baseline_yrs }}

-

{{plottype_title}} - {{var_title}}

+

{{ plottype_title }} - {{ var_title }}

diff --git a/lib/website_templates/template_index.html b/lib/website_templates/template_index.html index 1399cf280..83e45724d 100644 --- a/lib/website_templates/template_index.html +++ b/lib/website_templates/template_index.html @@ -11,7 +11,10 @@

-

Test Case:

{{ case1 }}

- years: {{ case_yrs }}


-

Baseline Case:

{{ case2 }}

- years: {{ baseline_yrs }}

+

Test Case:

{{ case_name }}

- years: {{ case_yrs }}


+

Baseline Case:

{{ base_name }}

- years: {{ baseline_yrs }}

diff --git a/lib/website_templates/template_mean_diag.html b/lib/website_templates/template_mean_diag.html index ba48458dc..c421a00e4 100644 --- a/lib/website_templates/template_mean_diag.html +++ b/lib/website_templates/template_mean_diag.html @@ -12,10 +12,13 @@
-

Test Case:

{{ case1 }}

- years: {{ case_yrs }}


-

Baseline Case:

{{ case2 }}

- years: {{ baseline_yrs }}

+

Test Case:

{{ case_name }}

- years: {{ case_yrs }}


+

Baseline Case:

{{ base_name }}

- years: {{ baseline_yrs }}

-

{{ curr_type }} Plots

+

{{ plottype_title }} Plots

{% for category, var_seas in mydata.items() %}
-
@@ -42,13 +61,13 @@

AMWG Tables

- - {% for case_name, html_file in amwg_tables.items() %} - - - - {% endfor %} -
{{ case_name }}
+ + {% for case_name, html_file in amwg_tables.items() %} + + + + {% endfor %} +
{{ case_name }}
diff --git a/lib/website_templates/template_multi_case.html b/lib/website_templates/template_multi_case.html new file mode 100644 index 000000000..e8836ea44 --- /dev/null +++ b/lib/website_templates/template_multi_case.html @@ -0,0 +1,81 @@ + + + + ADF {{ var_title }} + + + +
+
+

{{ title }}

+
+ +
+

Click on case name for that ADF case vs baseline page

+ {% for case_name, case_dtls in case_sites.items() %} +

Test Case {{ loop.index }}:

+

{{ case_name }}

+

- years: {{ case_dtls[1] }} - {{ case_dtls[2] }}


+ {% endfor %} +

Baseline Case:

+

{{ base_name }}

+

- years: {{ baseline_yrs }}

+
+ +
+
+
+

{{ plottype_title }} - {{ var_title }}

+
+ +
+
+ +
+
+ {% for category, var_seas in mydata.items() %} + {% for var_name, seas_files in var_seas.items() %} + {% if var_name == var_title %} + {% for season_name, file_name in seas_files.items() %} + + {% endfor %} + {% endif %} + {% endfor %} + {% endfor %} +
+ + +
+ + + diff --git a/lib/website_templates/template_multi_case_index.html b/lib/website_templates/template_multi_case_index.html index 0ba930dec..1640096e3 100644 --- a/lib/website_templates/template_multi_case_index.html +++ b/lib/website_templates/template_multi_case_index.html @@ -1,35 +1,52 @@ - ADF diagnostics + ADF Diagnostics - Multi-Case +
+
+

{{ title }}

+
+ +
+

Click on case name for that ADF case vs baseline page

+ {% for case_name, case_dtls in case_sites.items() %} +

Test Case {{ loop.index }}:

+

{{ case_name }}

+

- years: {{ case_dtls[1] }} - {{ case_dtls[2] }}


+ {% endfor %} +

Baseline Case:

+

{{ base_name }}

+

- years: {{ baseline_yrs }}

+
+
- +
+

All Case Comparison Plot Types

+
-

{{title}}

- - - {% for case_name, case_path in case_sites.items() %} - - - +
+ {% for type, html_file in multi_plots.items() %} + {% endfor %} -
{{ case_name }}
+
diff --git a/lib/website_templates/template_multi_case_mean_diag.html b/lib/website_templates/template_multi_case_mean_diag.html new file mode 100644 index 000000000..86f151d08 --- /dev/null +++ b/lib/website_templates/template_multi_case_mean_diag.html @@ -0,0 +1,69 @@ + + + + ADF Mean Plots + + + +
+
+

{{ title }}

+
+ +
+

Click on case name for that ADF case vs baseline page

+ {% for case_name, case_dtls in case_sites.items() %} +

Test Case {{ loop.index }}:

+

{{ case_name }}

+

- years: {{ case_dtls[1] }} - {{ case_dtls[2] }}


+ {% endfor %} +

Baseline Case:

+

{{ base_name }}

+

- years: {{ baseline_yrs }}

+
+
+ +
+

All Case Comparison - {{ plottype_title }} Plots

+
+ +
+ {% for category, var_seas in mydata.items() %} +
+ +
+ {% endfor %} +
+ + + diff --git a/lib/website_templates/template_multi_case_var.html b/lib/website_templates/template_multi_case_var.html new file mode 100644 index 000000000..5fa596122 --- /dev/null +++ b/lib/website_templates/template_multi_case_var.html @@ -0,0 +1,74 @@ + + + + ADF {{ var_title }} + + + +
+
+

{{ title }}

+
+ +
+

Click on case name for that ADF case vs baseline page

+ {% for case_name, case_dtls in case_sites.items() %} +

Test Case {{loop.index}}:

+

{{ case_name }}

+

- years: {{ case_dtls[1] }} - {{ case_dtls[2] }}


+ {% endfor %} +

Baseline Case:

+

{{ base_name }}

+

- years: {{ baseline_yrs }}

+
+
+ +
+
+
+

Multi-Case Comparison: {{ plottype_title }} - {{ var_title }}

+
+ +
+
+ +
+
+ {% for category, var_seas in mydata.items() %} + {% for var_name, seas_files in var_seas.items() %} + {% if var_name == var_title %} + {% for season_name, file_name in seas_files.items() %} + + {% endfor %} + {% endif %} + {% endfor %} + {% endfor %} +
+
+ + + diff --git a/lib/website_templates/template_table.html b/lib/website_templates/template_table.html index 7a00ccca2..be10f82fc 100644 --- a/lib/website_templates/template_table.html +++ b/lib/website_templates/template_table.html @@ -7,15 +7,22 @@
-

{{ title }}

+

{{ title }}

+ {% if multi==True %} +

Click on case name for that ADF case vs baseline page

+ {% for case_name, case_dtls in case_sites.items() %} +

Test Case {{ loop.index }}:

+

{{ case_name }}

+

- years: {{ case_dtls[1] }} - {{ case_dtls[2] }}


+ {% endfor %} +

Baseline Case:

+

{{ base_name }}

+

- years: {{ baseline_yrs }}

+ {% else %}
-

Test Case:

{{ case1 }}

- years: {{ case_yrs }}


-

Baseline Case:

{{ case2 }}

- years: {{ baseline_yrs }}

+

Test Case:

{{ case_name }}

- years: {{ case_yrs }}


+

Baseline Case:

{{ base_name }}

- years: {{ baseline_yrs }}

+ {% endif %}
@@ -42,13 +61,13 @@

AMWG Tables

- - {% for case_name, html_file in amwg_tables.items() %} - - - - {% endfor %} -
{{ case_name }}
+ + {% for case_name, html_file in amwg_tables.items() %} + + + + {% endfor %} +
{{ case_name }}

{{ table_name }}

diff --git a/lib/website_templates/template_var.html b/lib/website_templates/template_var.html index cc146f134..c205ddea0 100644 --- a/lib/website_templates/template_var.html +++ b/lib/website_templates/template_var.html @@ -1,7 +1,7 @@ - ADF {{var_title}} + ADF {{ var_title }} @@ -12,10 +12,13 @@

-

Test Case:

{{ case1 }}

- years: {{ case_yrs }}


-

Baseline Case:

{{ case2 }}

- years: {{ baseline_yrs }}

+

Test Case:

{{ case_name }}

- years: {{ case_yrs }}


+

Baseline Case:

{{ base_name }}

- years: {{ baseline_yrs }}

diff --git a/scripts/analysis/amwg_table.py b/scripts/analysis/amwg_table.py index 7a879cd5c..ea7ec13e3 100644 --- a/scripts/analysis/amwg_table.py +++ b/scripts/analysis/amwg_table.py @@ -60,6 +60,8 @@ def amwg_table(adf): #Import necessary modules: from adf_base import AdfError + + #Additional information: #---------------------- @@ -130,6 +132,10 @@ def amwg_table(adf): #CAM simulation variables (these quantities are always lists): case_names = adf.get_cam_info("cam_case_name", required=True) + + #Grab all case nickname(s) + test_nicknames = adf.case_nicknames["test_nicknames"] + input_ts_locs = adf.get_cam_info("cam_ts_loc", required=True) #Check if a baseline simulation is also being used: @@ -138,6 +144,11 @@ def amwg_table(adf): baseline_name = adf.get_baseline_info("cam_case_name", required=True) input_ts_baseline = adf.get_baseline_info("cam_ts_loc", required=True) + #Grab baseline case nickname + base_nickname = adf.case_nicknames["base_nickname"] + + test_nicknames += [base_nickname] + if "CMIP" in baseline_name: print("CMIP files detected, skipping AMWG table (for now)...") @@ -154,6 +165,9 @@ def amwg_table(adf): #residual top of model (RESTOM) radiation calculation: restom_dict = {} + #Hold output paths for csv files + csv_locs = [] + #Loop over CAM cases: for case_idx, case_name in enumerate(case_names): @@ -163,6 +177,9 @@ def amwg_table(adf): #Generate input file path: input_location = Path(input_ts_locs[case_idx]) + #Add output paths for csv files + csv_locs.append(output_locs[case_idx]) + #Check that time series input directory actually exists: if not input_location.is_dir(): errmsg = f"Time series directory '{input_location}' not found. Script is exiting." @@ -333,6 +350,18 @@ def amwg_table(adf): df.to_csv(output_csv_file, header=cols, index=False) #End if + #last step is to add table dataframe to website (if enabled): + table_df = pd.read_csv(output_csv_file) + + #Reorder RESTOM to top of tables + idx = table_df.index[table_df['variable'] == 'RESTOM'].tolist()[0] + table_df = pd.concat([table_df[table_df['variable'] == 'RESTOM'], table_df]).reset_index(drop = True) + table_df = table_df.drop([idx+1]).reset_index(drop=True) + table_df = table_df.drop_duplicates() + + #Re-save the csv file + table_df.to_csv(output_csv_file, header=cols, index=False) + else: #Print message to debug log: adf.debug_log("RESTOM not calculated because FSNT and/or FLNT variables not in dataset") @@ -344,7 +373,7 @@ def amwg_table(adf): #End of model case loop #---------------------- - + test_case_names = adf.get_cam_info("cam_case_name", required=True) #Notify user that script has ended: print(" ...AMWG variable table has been generated successfully.") @@ -354,10 +383,20 @@ def amwg_table(adf): if "CMIP" in baseline_name: print("CMIP case detected, skipping comparison table...") else: - #Create comparison table for both cases - print("\n Making comparison table...") - _df_comp_table(adf, output_location, case_names) - print(" ... Comparison table has been generated successfully") + + if len(test_case_names) == 1: + #Create comparison table for both cases + print("\n Making comparison table...") + _df_comp_table(adf, output_location, Path(output_locs[0]), case_names) + print(" ... Comparison table has been generated successfully") + + if len(test_case_names) > 1: + print("\n Making comparison table for multiple cases...") + _df_multi_comp_table(adf, csv_locs, case_names, test_nicknames) + print("\n Making comparison table for each case...") + for idx,case in enumerate(case_names[0:-1]): + _df_comp_table(adf, Path(output_locs[idx]), Path(output_locs[0]), [case,baseline_name]) + print(" ... Multi-case comparison table has been generated successfully") #End if else: print(" Comparison table currently doesn't work with obs, so skipping...") @@ -432,16 +471,18 @@ def _get_row_vals(data): ##### -def _df_comp_table(adf, output_location, case_names): - import pandas as pd +def _df_comp_table(adf, output_location, base_output_location, case_names): + """ + Function to build case vs baseline AMWG table + ----- + - Read in table data and create side by side comparison table + - Write output to csv file and add to website + """ output_csv_file_comp = output_location / "amwg_table_comp.csv" - # * * * * * * * * * * * * * * * * * * * * * * * * * * * * - #This will be for single-case for now (case_names[0]), - #will need to change to loop as multi-case is introduced case = output_location/f"amwg_table_{case_names[0]}.csv" - baseline = output_location/f"amwg_table_{case_names[-1]}.csv" + baseline = base_output_location/f"amwg_table_{case_names[-1]}.csv" #Read in test case and baseline dataframes: df_case = pd.read_csv(case) @@ -460,11 +501,82 @@ def _df_comp_table(adf, output_location, case_names): df_comp['diff'] = [f'{i:.3g}' if np.abs(i) < 1 else f'{i:.3f}' for i in diffs] #Write the comparison dataframe to a new CSV file: - cols_comp = ['variable', 'unit', 'test', 'control', 'diff'] + cols_comp = ['variable', 'unit', 'test', 'baseline', 'diff'] + df_comp.to_csv(output_csv_file_comp, header=cols_comp, index=False) + + #Add comparison table dataframe to website (if enabled): + adf.add_website_data(df_comp, "case_comparison", case_names[0], plot_type="Tables") + +##### + +def _df_multi_comp_table(adf, csv_locs, case_names, test_nicknames): + """ + Function to build comparison AMWG table for all cases + ------ + - Read in each previously made table from file + and compile full comparison. + """ + + #Create path to main website in mutli-case directory + main_site_path = Path(adf.get_basic_info('cam_diag_plot_loc', required=True)) + output_csv_file_comp = main_site_path / "amwg_table_comp_all.csv" + + #Create the "comparison" dataframe: + df_comp = pd.DataFrame(dtype=object) + + #Create new colummns + cols_comp = ['variable', 'unit'] + + #Read baseline case + baseline = str(csv_locs[-1])+f"/amwg_table_{case_names[-1]}.csv" + df_base = pd.read_csv(baseline) + + #Read all test cases and add to table + for i,val in enumerate(csv_locs[:-1]): + case = str(val)+f"/amwg_table_{case_names[i]}.csv" + df_case = pd.read_csv(case) + + #If no custom nicknames, shorten column name to case number + if test_nicknames[i] == case_names[i]: + df_comp[['variable','unit',f"case {i+1}"]] = df_case[['variable','unit','mean']] + cols_comp.append(f"case {i+1}") + #Else, name columns after nicknames + else: + df_comp[['variable','unit',f"{test_nicknames[i]}"]] = df_case[['variable','unit','mean']] + cols_comp.append(test_nicknames[i]) + + #Add baseline cases to end of the table + if test_nicknames[-1] == case_names[-1]: + df_comp["baseline"] = df_base[['mean']] + cols_comp.append("baseline") + else: + df_comp[f"{test_nicknames[-1]} ( baseline )"] = df_base[['mean']] + cols_comp.append(f"{test_nicknames[-1]} ( baseline )") + + #Format the floats: + for col in df_comp.columns: + #Ignore columns that don't contain floats + if (col != 'variable') and (col != "unit"): + if "baseline" not in col: + + #Iterate over rows and check magnitude of value + for idx,row in enumerate(df_comp[col]): + #Check if value is less than one, keep 3 non-zero decimal values + #Else, keep 3 main digits, including decimal values + if np.abs(df_comp[col][idx]) < 1: + formatter = ".3g" + else: + formatter = ".3f" + #Replace value in dataframe + df_comp.at[idx,col]= f'{df_comp[col][idx]:{formatter}} ({(df_comp[col][idx]-df_base["mean"][idx]):{formatter}})' + + #Finally, write data to csv df_comp.to_csv(output_csv_file_comp, header=cols_comp, index=False) #Add comparison table dataframe to website (if enabled): - adf.add_website_data(df_comp, "Case Comparison", case_names[0], plot_type="Tables") + adf.add_website_data(df_comp, "all_case_comparison", case_names[0], plot_type="Tables") + +##### ############## #END OF SCRIPT diff --git a/scripts/plotting/cam_taylor_diagram.py b/scripts/plotting/cam_taylor_diagram.py index 59b0d6c8b..a1c4c730c 100644 --- a/scripts/plotting/cam_taylor_diagram.py +++ b/scripts/plotting/cam_taylor_diagram.py @@ -11,6 +11,7 @@ # # --- imports and configuration --- # +from collections import OrderedDict from pathlib import Path import numpy as np import xarray as xr @@ -49,13 +50,23 @@ def cam_taylor_diagram(adfobj): # test case(s) == case(s) to be diagnosed will be called `case` (assumes a list) case_names = adfobj.get_cam_info('cam_case_name', required=True) # Loop over these + #Grab case climo years syear_cases = adfobj.climo_yrs["syears"] eyear_cases = adfobj.climo_yrs["eyears"] - #Grab test case nickname(s) - test_nicknames = adfobj.get_cam_info('case_nickname') - if test_nicknames == None: - test_nicknames = case_names + #Grab all case nickname(s) + test_nicknames = adfobj.case_nicknames["test_nicknames"] + base_nickname = adfobj.case_nicknames["base_nickname"] + + if len(case_names) > 1: + multi_case = True + + main_site_path = adfobj.main_site_paths["main_site_path"] + main_site_assets_path = adfobj.main_site_paths["main_site_assets_path"] + + else: + multi_case = False + #End if (check for multiple cases) case_climo_loc = adfobj.get_cam_info('cam_climo_loc', required=True) @@ -74,7 +85,10 @@ def cam_taylor_diagram(adfobj): plot_loc = Path(plot_location[0]) else: print(f"Ambiguous plotting location since all cases go on same plot. Will put them in first location: {plot_location[0]}") - plot_loc = Path(plot_location[0]) + if multi_case: + plot_loc = main_site_path + else: + plot_loc = Path(plot_location[0]) else: plot_loc = Path(plot_location) @@ -90,14 +104,9 @@ def cam_taylor_diagram(adfobj): data_name = adfobj.get_baseline_info('cam_case_name', required=True) data_list = data_name # should not be needed (?) data_loc = adfobj.get_baseline_info("cam_climo_loc", required=True) - - #Grab baseline case nickname - base_nickname = adfobj.get_baseline_info('case_nickname') - if base_nickname == None: - base_nickname = data_name #End if - #Extract baseline years (which may be empty strings if using Obs): + #Grab baseline years (which may be empty strings if using Obs): syear_baseline = adfobj.climo_yrs["syear_baseline"] eyear_baseline = adfobj.climo_yrs["eyear_baseline"] @@ -144,20 +153,6 @@ def cam_taylor_diagram(adfobj): # LOOP OVER SEASON # for s in seasons: - - plot_name = plot_loc / f"TaylorDiag_{s}_Special_Mean.{plot_type}" - print(f"\t - Plotting Taylor Diagram, {s}") - - # Check redo_plot. If set to True: remove old plot, if it already exists: - if (not redo_plot) and plot_name.is_file(): - #Add already-existing plot to website (if enabled): - adfobj.add_website_data(plot_name, "TaylorDiag", None, season=s, multi_case=True) - - #Continue to next iteration: - continue - elif (redo_plot) and plot_name.is_file(): - plot_name.unlink() - # hold the data in a DataFrame for each case # variable | correlation | stddev ratio | bias df_template = pd.DataFrame(index=var_list, columns=['corr', 'ratio', 'bias']) @@ -175,22 +170,85 @@ def cam_taylor_diagram(adfobj): # # -- PLOTTING (one per season) -- # + fig, ax = taylor_plot_setup(title=f"Taylor Diagram - {s}", - baseline=f"Baseline: {data_name} yrs: {syear_baseline}-{eyear_baseline}") + baseline=f"Baseline: {base_nickname} yrs: {syear_baseline}-{eyear_baseline}") for i, case in enumerate(case_names): ax = plot_taylor_data(ax, result_by_case[case], case_color=case_colors[i], use_bias=True) - ax = taylor_plot_finalize(ax, case_names, case_colors, syear_cases, eyear_cases, needs_bias_labels=True) + #If multi-case, make all individual case vs baseline taylor diagram + if multi_case: + fig_m, ax_m = taylor_plot_setup(title=f"Taylor Diagram - {s}", + baseline=f"Baseline: {base_nickname} yrs: {syear_baseline}-{eyear_baseline}") + ax_m = plot_taylor_data(ax_m, result_by_case[case], case_color=case_colors[i], use_bias=True) + ax_m = taylor_plot_finalize(ax_m, test_nicknames[i], case_colors[i], + syear_cases[i], eyear_cases[i], + needs_bias_labels=True,multi=True) + + # add text with variable names: + txtstrs = [f"{i+1} - {v}" for i, v in enumerate(var_list)] + fig_m.text(0.9, 0.9, "\n".join(txtstrs), va='top') + + plot_name = Path(plot_location[i]) / f"TaylorDiag_{s}_Special_Mean.{plot_type}" + fig_m.savefig(plot_name, bbox_inches='tight') + print(f"\t Taylor Diagram: completed {s}. \n\t File: {plot_name}") + plt.close() + + #Add plot to website (if enabled): + adfobj.add_website_data(plot_name, "TaylorDiag", case, category=None, season=s, plot_type = "Special") + #End if (multi-case) + #End for (cases) + + ax = taylor_plot_finalize(ax, test_nicknames, case_colors, syear_cases, eyear_cases, needs_bias_labels=True,multi=False) + #ax = taylor_plot_finalize(ax, case_names, case_colors, syear_cases, eyear_cases, needs_bias_labels=True) # add text with variable names: txtstrs = [f"{i+1} - {v}" for i, v in enumerate(var_list)] fig.text(0.9, 0.9, "\n".join(txtstrs), va='top') - fig.savefig(plot_name, bbox_inches='tight') - print(f"\t Taylor Diagram: completed {s}. \n\t File: {plot_name}") - #Add plot to website (if enabled): - adfobj.add_website_data(plot_name, "TaylorDiag", None, season=s, multi_case=True) + plot_name = plot_loc / f"TaylorDiag_{s}_Special_Mean.{plot_type}" + print(f"\t - Plotting Taylor Diagram, {s}") + + if multi_case: + plot_name = main_site_assets_path / f"TaylorDiag_{s}_Special_multi_plot.{plot_type}" + + # Check redo_plot. If set to True: remove old plot, if it already exists: + if (not redo_plot) and plot_name.is_file(): + #Add already-existing plot to website (if enabled): + adfobj.add_website_data(plot_name, "TaylorDiag", None, category=None, season=s, multi_case=True,plot_type = "Special") + + #Continue to next iteration: + continue + elif (redo_plot) and plot_name.is_file(): + plot_name.unlink() + + fig.savefig(plot_name, bbox_inches='tight') + plt.close() + + #Add plot to website (if enabled): + adfobj.add_website_data(plot_name, "TaylorDiag", None, category=None, season=s, multi_case=True,plot_type = "Special") + + print(" ...Taylor Diagram multi-case plots have been generated successfully.") + else: + plot_name = plot_loc / f"TaylorDiag_{s}_Special_Mean.{plot_type}" + + # Check redo_plot. If set to True: remove old plot, if it already exists: + if (not redo_plot) and plot_name.is_file(): + #Add already-existing plot to website (if enabled): + adfobj.add_website_data(plot_name, "TaylorDiag", case_names[0], category=None, season=s, plot_type = "Special") + #Continue to next iteration: + continue + elif (redo_plot) and plot_name.is_file(): + plot_name.unlink() + + fig.savefig(plot_name, bbox_inches='tight') + print(f"\t Taylor Diagram: completed {s}. \n\t File: {plot_name}") + plt.close() + + #Add plot to website (if enabled): + adfobj.add_website_data(plot_name, "TaylorDiag", case_names[0], category=None, season=s, plot_type = "Special") + #End if (multi-case check) #Notify user that script has ended: print(" ...Taylor Diagrams have been generated successfully.") @@ -561,7 +619,7 @@ def plot_taylor_data(wks, df, **kwargs): return wks -def taylor_plot_finalize(wks, casenames, casecolors, syear_cases, eyear_cases, needs_bias_labels=True): +def taylor_plot_finalize(wks, casenames, casecolors, syear_cases, eyear_cases, needs_bias_labels=True, multi=False): """Apply final formatting to a Taylor diagram. wks -> Axes object that has passed through taylor_plot_setup and plot_taylor_data casenames -> list of case names for the legend @@ -570,16 +628,23 @@ def taylor_plot_finalize(wks, casenames, casecolors, syear_cases, eyear_cases, n """ # CASE LEGEND -- Color-coded bottom_of_text = 0.05 - number_of_lines = len(casenames) height_of_lines = 0.03 - text = wks.text(0.052, 0.08, "Cases:", - color='k', ha='left', va='bottom', transform=wks.transAxes, fontsize=11) - n = 0 - for case_idx, (s, c) in enumerate(zip(casenames, casecolors)): - text = wks.text(0.052, bottom_of_text + n*height_of_lines, f"{s} yrs: {syear_cases[case_idx]}-{eyear_cases[case_idx]}", - color=c, ha='left', va='bottom', transform=wks.transAxes, fontsize=10) + case_pos = 0.75 + wks.text(0.99, case_pos, "Cases:", va='top', transform=wks.transAxes, fontsize=10) + + n = 0 + if multi: + for case_idx, (s, c) in enumerate(zip([casenames], [casecolors])): + text = wks.text(0.99, case_pos-((case_idx+1)*height_of_lines), f"{s} yrs: {syear_cases}-{eyear_cases}", + color=c, va='top', transform=wks.transAxes, fontsize=10) n += 1 + else: + for case_idx, (s, c) in enumerate(zip(casenames, casecolors)): + text = wks.text(0.99, case_pos-((case_idx+1)*height_of_lines), f"{s} yrs: {syear_cases[case_idx]}-{eyear_cases[case_idx]}", + color=c, va='top', transform=wks.transAxes, fontsize=10) + n += 1 + # BIAS LEGEND if needs_bias_labels: # produce an info-box showing the markers/sizes based on bias diff --git a/scripts/plotting/global_latlon_map.py b/scripts/plotting/global_latlon_map.py index f5a514cdd..4838cf40e 100644 --- a/scripts/plotting/global_latlon_map.py +++ b/scripts/plotting/global_latlon_map.py @@ -1,5 +1,6 @@ #Import standard modules: from pathlib import Path +from collections import OrderedDict import numpy as np import xarray as xr import warnings # use to warn user about missing files. @@ -64,15 +65,23 @@ def global_latlon_map(adfobj): #CAM simulation variables (this is always assumed to be a list): case_names = adfobj.get_cam_info("cam_case_name", required=True) + #read_config_var('multi_case_plots') + if len(case_names) > 1: + #Check if multi-plots are desired from yaml file + if adfobj.get_multi_case_info("global_latlon_map"): + multi_plots = True + multi_dict = OrderedDict() + else: + multi_plots = False + #End if (check for multi-case plots for LatLon) + else: + multi_plots = False + #End if (check for multiple cases) + #Grab case climo years syear_cases = adfobj.climo_yrs["syears"] eyear_cases = adfobj.climo_yrs["eyears"] - #Grab test case nickname(s) - test_nicknames = adfobj.get_cam_info('case_nickname') - if test_nicknames == None: - test_nicknames = case_names - # CAUTION: # "data" here refers to either obs or a baseline simulation, # Until those are both treated the same (via intake-esm or similar) @@ -81,29 +90,26 @@ def global_latlon_map(adfobj): #Extract variable-obs dictionary: var_obs_dict = adfobj.var_obs_dict - base_nickname = "Obs" #If dictionary is empty, then there are no observations to regrid to, #so quit here: if not var_obs_dict: print("No observations found to plot against, so no lat/lon maps will be generated.") return - else: data_name = adfobj.get_baseline_info("cam_case_name", required=True) # does not get used, is just here as a placemarker data_list = [data_name] # gets used as just the name to search for climo files HAS TO BE LIST data_loc = model_rgrid_loc #Just use the re-gridded model data path - - #Grab baseline case nickname - base_nickname = adfobj.get_baseline_info('case_nickname') - if base_nickname == None: - base_nickname = data_name #End if - #Extract baseline years (which may be empty strings if using Obs): + #Grab baseline years (which may be empty strings if using Obs): syear_baseline = adfobj.climo_yrs["syear_baseline"] eyear_baseline = adfobj.climo_yrs["eyear_baseline"] + #Grab all case nickname(s) + test_nicknames = adfobj.case_nicknames["test_nicknames"] + base_nickname = adfobj.case_nicknames["base_nickname"] + res = adfobj.variable_defaults # will be dict of variable-specific plot preferences # or an empty dictionary if use_defaults was not specified in YAML. @@ -145,6 +151,12 @@ def global_latlon_map(adfobj): # probably want to do this one variable at a time: for var in var_list: + #Check if multi-case scenario, if so grab details + if multi_plots: + for multi_var in adfobj.get_multi_case_info("global_latlon_map"): + if multi_var not in multi_dict: + multi_dict[multi_var] = OrderedDict() + if adfobj.compare_obs: #Check if obs exist for the variable: if var in var_obs_dict: @@ -198,6 +210,7 @@ def global_latlon_map(adfobj): oclim_fils = sorted(dclimo_loc.glob(f"{data_src}_{var}_baseline.nc")) oclim_ds = _load_dataset(oclim_fils) + if oclim_ds is None: print("WARNING: Did not find any oclim_fils. Will try to skip.") print(f"INFO: Data Location, dclimo_loc is {dclimo_loc}") @@ -207,6 +220,11 @@ def global_latlon_map(adfobj): #Loop over model cases: for case_idx, case_name in enumerate(case_names): + #Grab data for desired multi-plots (from yaml file) + if multi_plots: + if var in adfobj.get_multi_case_info("global_latlon_map"): + multi_dict[var][case_name] = OrderedDict() + #Set case nickname: case_nickname = test_nicknames[case_idx] @@ -218,7 +236,7 @@ def global_latlon_map(adfobj): print(" {} not found, making new directory".format(plot_loc)) plot_loc.mkdir(parents=True) - # load re-gridded model files: + #Load re-gridded model files: mclim_fils = sorted(mclimo_rg_loc.glob(f"{data_src}_{case_name}_{var}_*.nc")) mclim_ds = _load_dataset(mclim_fils) @@ -226,18 +244,18 @@ def global_latlon_map(adfobj): odata = oclim_ds[data_var].squeeze() # squeeze in case of degenerate dimensions mdata = mclim_ds[var].squeeze() - # APPLY UNITS TRANSFORMATION IF SPECIFIED: - # NOTE: looks like our climo files don't have all their metadata + #APPLY UNITS TRANSFORMATION IF SPECIFIED: + #NOTE: looks like our climo files don't have all their metadata mdata = mdata * vres.get("scale_factor",1) + vres.get("add_offset", 0) - # update units + #Update units mdata.attrs['units'] = vres.get("new_unit", mdata.attrs.get('units', 'none')) - # Do the same for the baseline case if need be: + #Do the same for the baseline case if need be: if not adfobj.compare_obs: odata = odata * vres.get("scale_factor",1) + vres.get("add_offset", 0) # update units odata.attrs['units'] = vres.get("new_unit", odata.attrs.get('units', 'none')) - # Or for observations: + #Or for observations: else: odata = odata * vres.get("obs_scale_factor",1) + vres.get("obs_add_offset", 0) # Note: we are going to assume that the specification ensures the conversion makes the units the same. Doesn't make sense to add a different unit. @@ -321,6 +339,11 @@ def global_latlon_map(adfobj): dseasons[s] = mseasons[s] - oseasons[s] #End if + #Grab data for desired multi-plots (from yaml file) + if multi_plots: + if var in adfobj.get_multi_case_info("global_latlon_map"): + multi_dict[var][case_name][s] = {"diff_data":dseasons[s],"vres":vres} + # time to make plot; here we'd probably loop over whatever plots we want for this variable # I'll just call this one "LatLon_Mean" ... would this work as a pattern [operation]_[AxesDescription] ? plot_name = plot_loc / f"{var}_{s}_LatLon_Mean.{plot_type}" @@ -328,7 +351,8 @@ def global_latlon_map(adfobj): # Check redo_plot. If set to True: remove old plot, if it already exists: if (not redo_plot) and plot_name.is_file(): #Add already-existing plot to website (if enabled): - adfobj.add_website_data(plot_name, var, case_name, category=web_category, + adfobj.add_website_data(plot_name, var, case_name, plot_ext="global_latlon_map", + category=web_category, season=s, plot_type="LatLon") #Continue to next iteration: @@ -347,11 +371,11 @@ def global_latlon_map(adfobj): pf.plot_map_and_save(plot_name, case_nickname, base_nickname, [syear_cases[case_idx],eyear_cases[case_idx]], [syear_baseline,eyear_baseline], - mseasons[s], oseasons[s], dseasons[s], - **vres) + mseasons[s], oseasons[s], dseasons[s], **vres) #Add plot to website (if enabled): - adfobj.add_website_data(plot_name, var, case_name, category=web_category, + adfobj.add_website_data(plot_name, var, case_name, plot_ext="global_latlon_map", + category=web_category, season=s, plot_type="LatLon") else: #mdata dimensions check @@ -426,6 +450,10 @@ def global_latlon_map(adfobj): dseasons[s] = mseasons[s] - oseasons[s] #End if + if multi_plots: + if var in adfobj.get_multi_case_info("global_latlon_map"): + multi_dict[var][case_name][s] = {"diff_data":dseasons[s],"vres":vres} + # time to make plot; here we'd probably loop over whatever plots we want for this variable # I'll just call this one "LatLon_Mean" ... would this work as a pattern [operation]_[AxesDescription] ? plot_name = plot_loc / f"{var}_{pres}hpa_{s}_LatLon_Mean.{plot_type}" @@ -434,8 +462,8 @@ def global_latlon_map(adfobj): redo_plot = adfobj.get_basic_info('redo_plot') if (not redo_plot) and plot_name.is_file(): #Add already-existing plot to website (if enabled): - adfobj.add_website_data(plot_name, f"{var}_{pres}hpa", case_name, category=web_category, - season=s, plot_type="LatLon") + adfobj.add_website_data(plot_name, f"{var}_{pres}hpa", case_name, plot_ext="global_latlon_map", + category=web_category, season=s, plot_type="LatLon") #Continue to next iteration: continue @@ -452,12 +480,11 @@ def global_latlon_map(adfobj): pf.plot_map_and_save(plot_name, case_nickname, base_nickname, [syear_cases[case_idx],eyear_cases[case_idx]], [syear_baseline,eyear_baseline], - mseasons[s], oseasons[s], dseasons[s], - **vres) + mseasons[s], oseasons[s], dseasons[s], **vres) #Add plot to website (if enabled): - adfobj.add_website_data(plot_name, f"{var}_{pres}hpa", case_name, category=web_category, - season=s, plot_type="LatLon") + adfobj.add_website_data(plot_name, f"{var}_{pres}hpa", case_name, plot_ext="global_latlon_map", + category=web_category, season=s, plot_type="LatLon") #End for (seasons) #End for (pressure levels) @@ -466,12 +493,24 @@ def global_latlon_map(adfobj): print(f"\t - variable '{var}' has no vertical dimension but is not just time/lat/lon, so skipping.") #End if (has_lev) else: - print(f"\t - skipping polar map for {var} as it has more than lat/lon dims, but no pressure levels were provided") + print(f"\t - skipping lat/lon map for {var} as it has more than lat/lon dims, but no pressure levels were provided") #End if (dimensions check and plotting pressure levels) #End for (case loop) #End for (obs/baseline loop) #End for (variable loop) + #This will be a list of variables for multi-case plotting based off LatLon plot type + if multi_plots: + #Notify user that script has started: + print("\n Generating lat/lon multi-case plots...") + + main_site_assets_path = adfobj.main_site_paths["main_site_assets_path"] + + pf.multi_latlon_plots(main_site_assets_path, "LatLon", case_names, + [test_nicknames,base_nickname], multi_dict, + web_category, adfobj) + + print(" ...lat/lon multi-case plots have been generated successfully.") #Notify user that script has ended: print(" ...lat/lon maps have been generated successfully.") @@ -492,4 +531,4 @@ def _load_dataset(fils): #End def ############## -#END OF SCRIPT +#END OF SCRIPT \ No newline at end of file diff --git a/scripts/plotting/global_latlon_vect_map.py b/scripts/plotting/global_latlon_vect_map.py index 8ba845c8d..14978c583 100644 --- a/scripts/plotting/global_latlon_vect_map.py +++ b/scripts/plotting/global_latlon_vect_map.py @@ -61,13 +61,13 @@ def global_latlon_vect_map(adfobj): #CAM simulation variables: case_names = adfobj.get_cam_info("cam_case_name", required=True) + #Grab case climo years syear_cases = adfobj.climo_yrs["syears"] eyear_cases = adfobj.climo_yrs["eyears"] - #Grab test case nickname(s) - test_nicknames = adfobj.get_cam_info('case_nickname') - if test_nicknames == None: - test_nicknames = case_names + #Grab all case nickname(s) + test_nicknames = adfobj.case_nicknames["test_nicknames"] + base_nickname = adfobj.case_nicknames["base_nickname"] # CAUTION: # "data" here refers to either obs or a baseline simulation, @@ -77,7 +77,6 @@ def global_latlon_vect_map(adfobj): #Extract variable-obs dictionary: var_obs_dict = adfobj.var_obs_dict - base_nickname = "Obs" #If dictionary is empty, then there are no observations to regrid to, #so quit here: @@ -89,14 +88,9 @@ def global_latlon_vect_map(adfobj): data_name = adfobj.get_baseline_info("cam_case_name", required=True) # does not get used, is just here as a placemarker data_list = [data_name] # gets used as just the name to search for climo files HAS TO BE LIST data_loc = model_rgrid_loc #Just use the re-gridded model data path - - #Grab baseline case nickname - base_nickname = adfobj.get_baseline_info('case_nickname') - if base_nickname == None: - base_nickname = data_name #End if - #Extract baseline years (which may be empty strings if using Obs): + #Grab baseline years (which may be empty strings if using Obs): syear_baseline = adfobj.climo_yrs["syear_baseline"] eyear_baseline = adfobj.climo_yrs["eyear_baseline"] diff --git a/scripts/plotting/meridional_mean.py b/scripts/plotting/meridional_mean.py index 1fadc2669..30363ca2d 100644 --- a/scripts/plotting/meridional_mean.py +++ b/scripts/plotting/meridional_mean.py @@ -36,13 +36,13 @@ def meridional_mean(adfobj): #CAM simulation variables (this is always assumed to be a list): case_names = adfobj.get_cam_info("cam_case_name", required=True) + #Grab case climo years syear_cases = adfobj.climo_yrs["syears"] eyear_cases = adfobj.climo_yrs["eyears"] - #Grab test case nickname(s) - test_nicknames = adfobj.get_cam_info('case_nickname') - if test_nicknames == None: - test_nicknames = case_names + #Grab all case nickname(s) + test_nicknames = adfobj.case_nicknames["test_nicknames"] + base_nickname = adfobj.case_nicknames["base_nickname"] # CAUTION: # "data" here refers to either obs or a baseline simulation, @@ -52,7 +52,6 @@ def meridional_mean(adfobj): #Extract variable-obs dictionary: var_obs_dict = adfobj.var_obs_dict - base_nickname = "Obs" #If dictionary is empty, then there are no observations to regrid to, #so quit here: @@ -64,14 +63,9 @@ def meridional_mean(adfobj): data_name = adfobj.get_baseline_info("cam_case_name", required=True) # does not get used, is just here as a placemarker data_list = [data_name] # gets used as just the name to search for climo files HAS TO BE LIST data_loc = model_rgrid_loc #Just use the re-gridded model data path - - #Grab baseline case nickname - base_nickname = adfobj.get_baseline_info('case_nickname') - if base_nickname == None: - base_nickname = data_name #End if - #Extract baseline years (which may be empty strings if using Obs): + #Grab baseline years (which may be empty strings if using Obs): syear_baseline = adfobj.climo_yrs["syear_baseline"] eyear_baseline = adfobj.climo_yrs["eyear_baseline"] diff --git a/scripts/plotting/polar_map.py b/scripts/plotting/polar_map.py index e4b6018a3..71e135e27 100644 --- a/scripts/plotting/polar_map.py +++ b/scripts/plotting/polar_map.py @@ -41,13 +41,13 @@ def polar_map(adfobj): #CAM simulation variables (this is always assumed to be a list): case_names = adfobj.get_cam_info("cam_case_name", required=True) + #Grab case climo years syear_cases = adfobj.climo_yrs["syears"] eyear_cases = adfobj.climo_yrs["eyears"] - #Grab test case nickname(s) - test_nicknames = adfobj.get_cam_info('case_nickname') - if test_nicknames == None: - test_nicknames = case_names + #Grab all case nickname(s) + test_nicknames = adfobj.case_nicknames["test_nicknames"] + base_nickname = adfobj.case_nicknames["base_nickname"] # CAUTION: # "data" here refers to either obs or a baseline simulation, @@ -57,7 +57,6 @@ def polar_map(adfobj): #Extract variable-obs dictionary: var_obs_dict = adfobj.var_obs_dict - base_nickname = "Obs" #If dictionary is empty, then there are no observations to regrid to, #so quit here: @@ -69,14 +68,9 @@ def polar_map(adfobj): data_name = adfobj.get_baseline_info("cam_case_name", required=True) # does not get used, is just here as a placemarker data_list = [data_name] # gets used as just the name to search for climo files HAS TO BE LIST data_loc = model_rgrid_loc #Just use the re-gridded model data path - - #Grab baseline case nickname - base_nickname = adfobj.get_baseline_info('case_nickname') - if base_nickname == None: - base_nickname = data_name #End if - #Extract baseline years (which may be empty strings if using Obs): + #Grab baseline years (which may be empty strings if using Obs): syear_baseline = adfobj.climo_yrs["syear_baseline"] eyear_baseline = adfobj.climo_yrs["eyear_baseline"] diff --git a/scripts/plotting/qbo.py b/scripts/plotting/qbo.py index 50c00df88..fa18b049f 100644 --- a/scripts/plotting/qbo.py +++ b/scripts/plotting/qbo.py @@ -32,7 +32,7 @@ def qbo(adfobj): print("\n Generating qbo plots...") #Extract relevant info from the ADF: - case_name = adfobj.get_cam_info('cam_case_name', required=True) + case_names = adfobj.get_cam_info('cam_case_name', required=True) case_loc = adfobj.get_cam_info('cam_ts_loc', required=True) base_name = adfobj.get_baseline_info('cam_case_name') base_loc = adfobj.get_baseline_info('cam_ts_loc') @@ -40,6 +40,20 @@ def qbo(adfobj): plot_locations = adfobj.plot_location plot_type = adfobj.get_basic_info('plot_type') + #Grab all case nickname(s) + test_nicknames = adfobj.case_nicknames["test_nicknames"] + base_nickname = adfobj.case_nicknames["base_nickname"] + case_nicknames = test_nicknames + [base_nickname] + + if len(case_names) > 1: + multi_case = True + main_site_assets_path = adfobj.main_site_paths["main_site_assets_path"] + else: + multi_case = False + #End if (check for multiple cases) + + + # check if existing plots need to be redone redo_plot = adfobj.get_basic_info('redo_plot') print(f"\t NOTE: redo_plot is set to {redo_plot}") @@ -66,25 +80,10 @@ def qbo(adfobj): #that the QBO plot will be kept in the first case directory: print(f"\t QBO plots will be saved here: {plot_locations[0]}") - # Check redo_plot. If set to True: remove old plots, if they already exist: - if (not redo_plot) and plot_loc_ts.is_file() and plot_loc_amp.is_file(): - #Add already-existing plot to website (if enabled): - adfobj.add_website_data(plot_loc_ts, "QBO", None, season="QBOts", multi_case=True) - adfobj.add_website_data(plot_loc_amp, "QBO", None, season="QBOamp", multi_case=True) - - #Continue to next iteration: - return - elif (redo_plot): - if plot_loc_ts.is_file(): - plot_loc_ts.unlink() - if plot_loc_amp.is_file(): - plot_loc_amp.unlink() - #End if - #Check if model vs model run, and if so, append baseline to case lists: if not adfobj.compare_obs: case_loc.append(base_loc) - case_name.append(base_name) + case_names.append(base_name) #End if #----Read in the OBS (ERA5, 5S-5N average already @@ -92,13 +91,14 @@ def qbo(adfobj): #----Read in the case data and baseline ncases = len(case_loc) - casedat = [ _load_dataset(case_loc[i], case_name[i],'U') for i in range(0,ncases,1) ] + + casedat = [ _load_dataset(case_loc[i], case_names[i],'U') for i in range(0,ncases,1) ] #Find indices for all case datasets that don't contain a zonal wind field (U): bad_idxs = [] for idx, dat in enumerate(casedat): if 'U' not in dat.variables: - warnings.warn(f"QBO: case {case_name[idx]} contains no 'U' field, skipping...") + warnings.warn(f"QBO: case {case_names[idx]} contains no 'U' field, skipping...") bad_idxs.append(idx) #End if #End for @@ -124,57 +124,198 @@ def qbo(adfobj): #----QBO timeseries plots fig = plt.figure(figsize=(16,16)) + fig.suptitle('QBO Time Series', fontsize=14) x1, x2, y1, y2 = plotpos() - ax = plotqbotimeseries(fig, obs, minny, x1[0], x2[0], y1[0], y2[0],'ERA5') + #ax = plotqbotimeseries(fig, obs, minny, x1[0], x2[0], y1[0], y2[0],'ERA5') + + if (adfobj.compare_obs) and (not multi_case): + xo1 = 0.18 + xo2 = 0.45 + xm1 = 0.55 + xm2 = 0.82 + ax = plotqbotimeseries(fig, obs, minny, xo1, xo2, y1[0], y2[0],'ERA5') + else: + ax = plotqbotimeseries(fig, obs, minny, x1[0], x2[0], y1[0], y2[0],'ERA5') - casecount=0 + #casecount=0 for icase in range(0,ncases,1): if (icase < 11 ): # only only going to work with 12 panels currently - ax = plotqbotimeseries(fig, casedat_5S_5N[icase],minny, - x1[icase+1],x2[icase+1],y1[icase+1],y2[icase+1], case_name[icase]) - casecount=casecount+1 + #Check if this is multi-case diagnostics + if multi_case: + if icase != ncases-1: + plot_loc_ts = Path(plot_locations[icase]) / f'QBOts.{plot_type}' + + #----QBO timeseries plots + fig_m = plt.figure(figsize=(16,16)) + fig_m.suptitle('QBO Time Series', fontsize=14) + + """#Plot ERA5 data + ax_m = plotqbotimeseries(fig_m, obs, minny, x1[0], x2[0], y1[0], y2[0],'ERA5')""" + + """#Plot individual case + ax_m = plotqbotimeseries(fig_m, casedat_5S_5N[icase],minny, + x1[1],x2[1],y1[1],y2[1], + case_nicknames[icase])""" + + #Check if compared vs baseline obs, alter x-positions + if adfobj.compare_obs: + """#Plot ERA5 data + ax_m = plotqbotimeseries(fig_m, obs, minny, 0.18, 0.45, y1[0], y2[0],'ERA5') + + #Plot individual case + ax_m = plotqbotimeseries(fig_m, casedat_5S_5N[icase],minny, + 0.55, 0.82, y1[2], y2[2], + case_nicknames[icase])""" + + xo1 = 0.18 + xo2 = 0.45 + xm1 = 0.55 + xm2 = 0.82 + #No observation baseline + else: + """#Plot ERA5 data + ax_m = plotqbotimeseries(fig_m, obs, minny, x1[0], x2[0], y1[0], y2[0],'ERA5') + + #Plot individual case + ax_m = plotqbotimeseries(fig_m, casedat_5S_5N[icase],minny, + x1[1],x2[1],y1[1],y2[1], + case_nicknames[icase])""" + + xo1 = x1[0] + xo2 = x2[0] + xm1 = x1[1] + xm2 = x2[1] + + ax_m = plotqbotimeseries(fig_m, casedat_5S_5N[-1],minny, + x1[2],x2[2],y1[2],y2[2], + base_nickname) + #End if (compare obs) + + #Plot ERA5 data + ax_m = plotqbotimeseries(fig_m, obs, minny, xo1, xo2, y1[0], y2[0],'ERA5') + + #Plot individual case + ax_m = plotqbotimeseries(fig_m, casedat_5S_5N[icase],minny, + xm1, xm2, y1[2], y2[2], + case_nicknames[icase]) + + #Plot colorbar + ax_m = plotcolorbar(fig_m, x1[0]+0.2, x2[2]-0.2,y1[2]-0.035,y1[2]-0.03) + + #Save figure to file: + fig_m.savefig(plot_loc_ts, bbox_inches='tight', facecolor='white') + + #Add plot to website (if enabled): + adfobj.add_website_data(plot_loc_ts, "QBO", case_names[icase], category=None, season="QBOts", + multi_case=True,plot_type="Special") + #End if (multi-case) + + #Check if compared vs baseline obs, alter x-positions + if (adfobj.compare_obs) and (not multi_case): + ax = plotqbotimeseries(fig, casedat_5S_5N[icase],minny, + xm1,xm2,y1[icase+1],y2[icase+1], case_nicknames[icase]) + else: + ax = plotqbotimeseries(fig, casedat_5S_5N[icase],minny, + x1[icase+1],x2[icase+1],y1[icase+1],y2[icase+1], case_nicknames[icase]) + #casecount=casecount+1 else: warnings.warn("The QBO diagnostics can only manage up to twelve cases!") break #End if #End for + #ax = plotcolorbar(fig, x1[0]+0.2, x2[2]-0.2,y1[casecount]-0.035,y1[casecount]-0.03) + ax = plotcolorbar(fig, x1[0]+0.2, x2[2]-0.2,y1[ncases]-0.035,y1[ncases]-0.03) + + if multi_case:#Notify user that script has started: + print("\n Generating qbo multi-case plots...") + + + plot_loc_ts_multi = main_site_assets_path / f'QBO_QBOts_Special_multi_plot.{plot_type}' + fig.savefig(plot_loc_ts_multi, bbox_inches='tight', facecolor='white') + adfobj.add_website_data(plot_loc_ts_multi, "QBO", None, category=None, season="QBOts", + multi_case=True,plot_type="Special") + + else: + #Save figure to file: + fig.savefig(plot_loc_ts, bbox_inches='tight', facecolor='white') - ax = plotcolorbar(fig, x1[0]+0.2, x2[2]-0.2,y1[casecount]-0.035,y1[casecount]-0.03) - - #Save figure to file: - fig.savefig(plot_loc_ts, bbox_inches='tight', facecolor='white') - - #Add plot to website (if enabled): - adfobj.add_website_data(plot_loc_ts, "QBO", None, season="QBOts", multi_case=True) - + #Add plot to website (if enabled): + #adfobj.add_website_data(plot_loc_ts, "QBO", None, season="QBOts", multi_case=True,plot_type = "Special") #multi_case=True + adfobj.add_website_data(plot_loc_ts, "QBO", case_names[0], category=None, season="QBOts", + multi_case=True,plot_type="Special") #----------------- #---Dunkerton and Delisi QBO amplitude obsamp = calcddamp(obs) modamp = [ calcddamp(casedat_5S_5N[i]) for i in range(0,ncases,1) ] + if multi_case: + for icase in range(0,ncases,1): + #Skip baseline case + if icase != ncases-1: + fig = plt.figure(figsize=(16,16)) + + ax = fig.add_axes([0.05,0.6,0.4,0.4]) + ax.plot(modamp[icase], -np.log10(modamp[icase].lev), linewidth=2, label=case_nicknames[icase]) + + #Check to plot baseline if not compared to obs + if not adfobj.compare_obs: + ax.plot(modamp[-1], -np.log10(modamp[-1].lev), linewidth=2, label=base_nickname) + + ax.plot(obsamp, -np.log10(obsamp.pre), color='black', linewidth=2, label='ERA5') + + ax.set_ylim(-np.log10(150),-np.log10(1)) + ax.set_yticks([-np.log10(100),-np.log10(30),-np.log10(10),-np.log10(3),-np.log10(1)]) + ax.set_yticklabels(['100','30','10','3','1'], fontsize=12) + ax.set_ylabel('Pressure (hPa)', fontsize=12) + ax.set_xlabel('Dunkerton and Delisi QBO amplitude (ms$^{-1}$)', fontsize=12) + ax.set_title('Dunkerton and Delisi QBO amplitude', fontsize=14) + + ax.legend(loc='upper left') + + plot_loc_amp = Path(plot_locations[icase]) / f'QBOamp.{plot_type}' + + fig.savefig(plot_loc_amp, bbox_inches='tight', facecolor='white') + plt.close() + #Add plot to website (if enabled): + adfobj.add_website_data(plot_loc_amp, "QBO", case_names[icase], category = None, season="QBOamp", multi_case=True,plot_type = "Special") + #End if (not baseline) + #End for (cases) + #End if (multi-case) fig = plt.figure(figsize=(16,16)) ax = fig.add_axes([0.05,0.6,0.4,0.4]) + + ax.set_ylim(-np.log10(150),-np.log10(1)) ax.set_yticks([-np.log10(100),-np.log10(30),-np.log10(10),-np.log10(3),-np.log10(1)]) ax.set_yticklabels(['100','30','10','3','1'], fontsize=12) ax.set_ylabel('Pressure (hPa)', fontsize=12) ax.set_xlabel('Dunkerton and Delisi QBO amplitude (ms$^{-1}$)', fontsize=12) ax.set_title('Dunkerton and Delisi QBO amplitude', fontsize=14) - ax.plot(obsamp, -np.log10(obsamp.pre), color='black', linewidth=2, label='ERA5') - for icase in range(0,ncases,1): - ax.plot(modamp[icase], -np.log10(modamp[icase].lev), linewidth=2, label=case_name[icase]) + ax.plot(modamp[icase], -np.log10(modamp[icase].lev), linewidth=2, label=case_nicknames[icase]) ax.legend(loc='upper left') - fig.savefig(plot_loc_amp, bbox_inches='tight', facecolor='white') - #Add plot to website (if enabled): - adfobj.add_website_data(plot_loc_amp, "QBO", None, season="QBOamp", multi_case=True) + # + if multi_case: + plot_loc_amp_multi = main_site_assets_path / f'QBO_QBOamp_Special_multi_plot.{plot_type}' + fig.savefig(plot_loc_amp_multi, bbox_inches='tight', facecolor='white') + #Add plot to website (if enabled): + adfobj.add_website_data(plot_loc_amp_multi, "QBO", None, category=None, season="QBOamp", + multi_case=True,plot_type = "Special") + else: + fig.savefig(plot_loc_amp, bbox_inches='tight', facecolor='white') + + #Add plot to website (if enabled): + adfobj.add_website_data(plot_loc_amp, "QBO", case_names[0], category = None, season="QBOamp", multi_case=True,plot_type = "Special") + + #Close main fig + plt.close() #------------------- #Notify user that script has ended: @@ -282,8 +423,9 @@ def plotqbotimeseries(fig, dat, ny, x1, x2, y1, y2, title): ax.set_yticks([-np.log10(1000),-np.log10(300),-np.log10(100),-np.log10(30),-np.log10(10), -np.log10(3),-np.log10(1)]) ax.set_yticklabels(['1000','300','100','30','10','3','1']) - ax.set_ylabel('Pressure (hPa)') + ax.set_ylabel('Pressure (hPa)', fontsize=12) ax.set_title(title, fontsize=14) + #ax.set_xlabel("Years",fontsize=12,labelpad=10) return ax @@ -332,5 +474,17 @@ def blue2red_cmap(n, nowhite = False): return mymap +def _set_ymargin(ax, top, bottom): + """ + Allow for custom padding of plot lines and axes borders + ----- + """ + ax.set_ymargin(0) + ax.autoscale_view() + lim = ax.get_ylim() + delta = np.diff(lim) + top = lim[1] + delta*top + bottom = lim[0] - delta*bottom + ax.set_ylim(bottom,top) diff --git a/scripts/plotting/zonal_mean.py b/scripts/plotting/zonal_mean.py index 950996230..3343630df 100644 --- a/scripts/plotting/zonal_mean.py +++ b/scripts/plotting/zonal_mean.py @@ -60,13 +60,13 @@ def zonal_mean(adfobj): #CAM simulation variables (this is always assumed to be a list): case_names = adfobj.get_cam_info("cam_case_name", required=True) + #Grab case climo years syear_cases = adfobj.climo_yrs["syears"] eyear_cases = adfobj.climo_yrs["eyears"] - #Grab test case nickname(s) - test_nicknames = adfobj.get_cam_info('case_nickname') - if test_nicknames == None: - test_nicknames = case_names + #Grab all case nickname(s) + test_nicknames = adfobj.case_nicknames["test_nicknames"] + base_nickname = adfobj.case_nicknames["base_nickname"] # CAUTION: # "data" here refers to either obs or a baseline simulation, @@ -76,7 +76,6 @@ def zonal_mean(adfobj): #Extract variable-obs dictionary: var_obs_dict = adfobj.var_obs_dict - base_nickname = "Obs" #If dictionary is empty, then there are no observations to regrid to, #so quit here: @@ -88,14 +87,9 @@ def zonal_mean(adfobj): data_name = adfobj.get_baseline_info("cam_case_name", required=True) # does not get used, is just here as a placemarker data_list = [data_name] # gets used as just the name to search for climo files HAS TO BE LIST data_loc = model_rgrid_loc #Just use the re-gridded model data path - - #Grab baseline case nickname - base_nickname = adfobj.get_baseline_info('case_nickname') - if base_nickname == None: - base_nickname = data_name #End if - #Extract baseline years (which may be empty strings if using Obs): + #Grab baseline years (which may be empty strings if using Obs): syear_baseline = adfobj.climo_yrs["syear_baseline"] eyear_baseline = adfobj.climo_yrs["eyear_baseline"] diff --git a/scripts/regridding/regrid_and_vert_interp.py b/scripts/regridding/regrid_and_vert_interp.py index 9f70d1d24..c06956862 100644 --- a/scripts/regridding/regrid_and_vert_interp.py +++ b/scripts/regridding/regrid_and_vert_interp.py @@ -216,6 +216,9 @@ def regrid_and_vert_interp(adf): if len(mclim_fils) > 1: #Combine all cam files together into a single data set: mclim_ds = xr.open_mfdataset(mclim_fils, combine='by_coords') + elif len(mclim_fils) == 0: + print(f"\t - regridding {var} failed, no file. Continuing to next variable.") + continue else: #Open single file as new xarray dataset: mclim_ds = xr.open_dataset(mclim_fils[0])