diff --git a/.github/scripts/pr_mod_file_tests.py b/.github/scripts/pr_mod_file_tests.py index 150febff8..ffc68c061 100755 --- a/.github/scripts/pr_mod_file_tests.py +++ b/.github/scripts/pr_mod_file_tests.py @@ -7,8 +7,9 @@ Github Pull Request (PR), using the PyGithub interface, and then to run tests on those files when appropriate. -Note: This version currently limit the tests to a subset of files, - in order to avoid running pylint on non-core python source files. +Note: This version currently limits the tests to the python files + under the "lib" directory, in order to avoid running pylint on + non-core python source files. Written by: Jesse Nusbaumer - November, 2020 """ @@ -24,7 +25,9 @@ import argparse from stat import S_ISREG -from github import Github +from pathlib import Path + +from github import Auth, Github #Local scripts: from pylint_threshold_test import pylint_check @@ -93,6 +96,32 @@ def _file_is_python(filename): #Return file type result: return is_python +################# + +def _file_is_testable(filename, testable_dir, excluded_dirs): + + """ + Checks whether a given file lives underneath a + directory whose python files should be linted, while + also skipping any files that live underneath one of + the excluded directories. + """ + + #Determine all directories that contain this file: + file_parents = Path(filename).parents + + #File must live somewhere underneath the testable directory: + if Path(testable_dir) not in file_parents: + return False + + #File must not live underneath an excluded directory: + for excluded_dir in excluded_dirs: + if Path(excluded_dir) in file_parents: + return False + + #If both checks pass, then the file is testable: + return True + #++++++++++++++++++++++++++++++ #Input Argument parser function #++++++++++++++++++++++++++++++ @@ -140,18 +169,15 @@ def _main_prog(): print("Generating list of modified files...") - # This should eventually be passed in via a command-line - # argument, and include everything inside the "lib" directory -JN: - testable_files = { - "lib/adf_base.py", - "lib/adf_config.py", - "lib/adf_file_utils.py", - "lib/adf_info.py", - "lib/adf_obs.py", - "lib/adf_units.py", - "lib/adf_web.py", - "lib/adf_diag.py", - } + #All python files underneath this directory are linted. This + #should eventually be passed in via a command-line argument -JN: + testable_dir = "lib" + + #Directories underneath "testable_dir" that should never be linted. + #The "lib/externals" directory contains code copied in from other + #projects (e.g. CVDP), which needs to stay identical to its upstream + #source. + excluded_dirs = {"lib/externals"} #+++++++++++++++++++++++ #Read in input arguments @@ -169,7 +195,7 @@ def _main_prog(): #Log-in to github API using token #++++++++++++++++++++++++++++++++ - ghub = Github(token) + ghub = Github(auth=Auth.Token(token)) #++++++++++++++++++++ #Open ESCOMP/CAM repo @@ -189,7 +215,7 @@ def _main_prog(): #++++++++++++++++++++++++++++++ #Create empty list to store python files: - pyfiles = list() + pyfiles = [] #Extract Github file objects: file_obj_list = pull_req.get_files() @@ -215,7 +241,7 @@ def _main_prog(): # users of python files that will be tested: lint_files = [] for pyfile in pyfiles: - if pyfile in testable_files: + if _file_is_testable(pyfile, testable_dir, excluded_dirs): lint_files.append(pyfile) else: continue @@ -261,9 +287,10 @@ def _main_prog(): print("All pylint tests passed!") sys.exit(0) - #If no python files in set of testable_files, then exit script: + #If no python files are underneath "testable_dir", then exit script: else: - print("No ADF classes were modified in PR, so there is nothing to test.") + print(f"No python files under '{testable_dir}' were modified in PR, " + "so there is nothing to test.") sys.exit(0) #End if (lint_files) diff --git a/.github/workflows/ADF_linting.yaml b/.github/workflows/ADF_linting.yaml index bee7aeb7e..1bf882c92 100644 --- a/.github/workflows/ADF_linting.yaml +++ b/.github/workflows/ADF_linting.yaml @@ -24,11 +24,18 @@ jobs: # install required python packages - name: Install dependencies run: | - python -m pip install --upgrade pip # Install latest version of PIP - pip install PyGithub # Install PyGithub python package - pip install pylint # Install Pylint python package - pip install pyyaml # Install PyYAML python package - pip install numpy # Install NumPy python package + python -m pip install --upgrade pip + pip install PyGithub + pip install pylint + pip install pyyaml + pip install numpy + pip install xarray + pip install pandas + pip install matplotlib + pip install cartopy + pip install geocat-comp + pip install markdown + pip install jinja2 # run CAM source code testing master script: - name: source-code testing python script env: diff --git a/lib/adf_base.py b/lib/adf_base.py index 08d30a2ba..341f2507f 100644 --- a/lib/adf_base.py +++ b/lib/adf_base.py @@ -41,7 +41,7 @@ class AdfBase: def __init__(self, debug=False): """ - Initalize CAM diagnostics object. + Initialize CAM diagnostics object. """ # Check that debug is in fact a boolean, diff --git a/lib/adf_config.py b/lib/adf_config.py index 74baf78fa..a41f82413 100644 --- a/lib/adf_config.py +++ b/lib/adf_config.py @@ -187,7 +187,7 @@ def __expand_yaml_var_ref(self, var_val): # -------------------------- # Throw an error if keyword not in dictionary: - if kword_match_str_key not in self.__search_dict.keys(): + if kword_match_str_key not in self.__search_dict: ermsg = f"ERROR: Variable '{kword_match_str}'" ermsg += " not found in config (YAML) file." self.end_diag_fail(ermsg) diff --git a/lib/adf_dataset.py b/lib/adf_dataset.py index 9b3555902..9366a7a9c 100644 --- a/lib/adf_dataset.py +++ b/lib/adf_dataset.py @@ -50,7 +50,7 @@ # apply scaling. -class AdfData: +class AdfData: # pylint: disable=too-many-public-methods """A class instantiated with an AdfDiag object. Methods provide means to load data. This class does not interact with plotting, diff --git a/lib/adf_derive.py b/lib/adf_derive.py index 25bdc8e46..1fde161b5 100644 --- a/lib/adf_derive.py +++ b/lib/adf_derive.py @@ -642,7 +642,7 @@ def derive_variable( ds = self.data.load_dataset(constit_files) if not ds: dmsg = f"derived time series for {case_name}:" - dmsg += f"\n\tNo files to open." + dmsg += "\n\tNo files to open." self.debug_log(dmsg) return diff --git a/lib/adf_diag.py b/lib/adf_diag.py index 812d3b01e..02658535c 100644 --- a/lib/adf_diag.py +++ b/lib/adf_diag.py @@ -690,7 +690,8 @@ def run_pool(commands, label): for hist_str in hist_str_case: print( - f"\t Processing time series for {case_type_string} {case_name}, {hist_str} files:" + f"\t Processing time series for {case_type_string} {case_name}," + f" {hist_str} files:" ) if not list(starting_location.glob("*" + hist_str + ".*.nc")): emsg = f"No history *{hist_str}.*.nc files found in '{starting_location}'." diff --git a/lib/adf_file_utils.py b/lib/adf_file_utils.py index fb9a5641b..7e62f1b50 100644 --- a/lib/adf_file_utils.py +++ b/lib/adf_file_utils.py @@ -207,7 +207,7 @@ def find_ts_files(ts_loc, pattern, recursive=True): return sorted(ts_loc.rglob(pattern)) -def select_ts_files(fils, syr, eyr): +def select_ts_files(fils, syr, eyr): # pylint: disable=too-many-return-statements """ Narrow a set of time series files to those needed for a year range. diff --git a/lib/adf_gents.py b/lib/adf_gents.py index 9dab4f17a..927943231 100644 --- a/lib/adf_gents.py +++ b/lib/adf_gents.py @@ -33,6 +33,10 @@ import sys from pathlib import Path +# +++++++++++++++++++++++++++++++++++++++++++++++++ +# import non-standard python modules, including ADF +# +++++++++++++++++++++++++++++++++++++++++++++++++ + import xarray as xr # ADF modules: @@ -177,7 +181,8 @@ def create_time_series_gents(adf, baseline=False): from :mod:`adf_file_utils`. """ - HFCollection, TSCollection = _import_gents() + # These are classes, so keep their PascalCase names: + HFCollection, TSCollection = _import_gents() # pylint: disable=invalid-name # Notify user that script has started: msg = "\n Calculating CAM time series with GenTS..." @@ -344,7 +349,7 @@ def create_time_series_gents(adf, baseline=False): tsc = _restrict_to_vars(tsc, wanted_vars) # End if - if not len(tsc): + if not tsc: wmsg = ( f"\t WARNING: GenTS found nothing to generate for '{hist_str}'." ) diff --git a/lib/adf_info.py b/lib/adf_info.py index 96bf77bcb..ae73201c6 100644 --- a/lib/adf_info.py +++ b/lib/adf_info.py @@ -32,7 +32,6 @@ import copy import os import getpass -import subprocess # +++++++++++++++++++++++++++++++++++++++++++++++++ # import non-standard python modules, including ADF diff --git a/lib/adf_obs.py b/lib/adf_obs.py index c61c073d3..4ab0fee98 100644 --- a/lib/adf_obs.py +++ b/lib/adf_obs.py @@ -3,7 +3,7 @@ Diagnostics Framework (ADF). This class inherits from the AdfInfo class. -Currently this class does three things: +Currently this class does four things: 1. Initializes an instance of AdfInfo. diff --git a/lib/adf_units.py b/lib/adf_units.py index 46308d474..2306be1fc 100644 --- a/lib/adf_units.py +++ b/lib/adf_units.py @@ -26,7 +26,7 @@ asked, and getting it wrong scales the data twice. Rendering a unit for somewhere with no LaTeX renderer, such as a table cell, -is `adf_utils.plain_text_units`; this module only compares. +is :func:`adf_utils.plain_text_units`; this module only compares. """ import re diff --git a/lib/adf_utils.py b/lib/adf_utils.py index 0d5afb3f9..ccc70d3ef 100644 --- a/lib/adf_utils.py +++ b/lib/adf_utils.py @@ -37,7 +37,8 @@ lev_to_plev(data, ps, hyam, hybm, P0=100000., new_levels=None, convert_to_mb=False) Interpolate model hybrid levels to specified pressure levels. pmid_to_plev(data, pmid, new_levels=None, convert_to_mb=False) - Interpolate `data` from hybrid-sigma levels to isobaric levels using provided mid-level pressures. + Interpolate `data` from hybrid-sigma levels to isobaric levels using provided + mid-level pressures. plev_to_plev(data, new_levels=None, convert_to_mb=False) Interpolate `data` from isobaric levels to new isobaric levels. zonal_mean_xr(fld) @@ -54,6 +55,19 @@ """ +# This module collects many small helpers, so allow it to be long: +# pylint: disable=too-many-lines + +# ++++++++++++++++++++++++++++++ +# Import standard python modules +# ++++++++++++++++++++++++++++++ + +import warnings # use to warn user about missing files. + +# +++++++++++++++++++++++++++++++++++++++++++++++++ +# import non-standard python modules, including ADF +# +++++++++++++++++++++++++++++++++++++++++++++++++ + # import statements: import numpy as np import xarray as xr @@ -78,13 +92,9 @@ ts_var_from_filename, ) -# pylint: enable=unused-import - -import warnings # use to warn user about missing files. - # Format warning messages: -def my_formatwarning(msg, *args, **kwargs): +def my_formatwarning(msg, *_args, **_kwargs): """Issue `msg` as warning.""" return str(msg) + "\n" @@ -220,7 +230,8 @@ def use_time_bounds_midpoint(ds, time_name="time"): def load_dataset(fils, use_time_bounds=False): """ - This method exists to get an xarray Dataset from input file information that can be passed into the plotting methods. + This method exists to get an xarray Dataset from input file information that + can be passed into the plotting methods. Parameters ---------- @@ -240,9 +251,9 @@ def load_dataset(fils, use_time_bounds=False): year. See `use_time_bounds_midpoint`. """ if len(fils) == 0: - warnings.warn(f"\t WARNING: Input file list is empty.") + warnings.warn("\t WARNING: Input file list is empty.") return None - elif len(fils) > 1: + if len(fils) > 1: ds = xr.open_mfdataset(fils, combine="by_coords") else: ds = xr.open_dataset(fils[0]) @@ -575,7 +586,7 @@ def request_pressure_field_from_ts(adf, ts_dir, case_name, hist_strs=None): if names is False: return [] streams = as_hist_str_list(hist_strs) or ["*"] - wanted = [v for v in adf.diag_var_list] + wanted = list(adf.diag_var_list) added = [] for level_dim in ("lev", "ilev"): name = pressure_field_name(level_dim, names) @@ -675,18 +686,13 @@ def global_average(fld, wgt, verbose=False): fld2 = np.ma.masked_invalid(fld) if verbose: print( - "(global_average)-- fraction of mask that is True: {}".format( - np.count_nonzero(fld2.mask) / np.size(fld2) - ) + "(global_average)-- fraction of mask that is True: " + f"{np.count_nonzero(fld2.mask) / np.size(fld2)}" ) print( - "(global_average)-- apply ma.average along axis = {} // validate: {}".format( - a, fld2.shape - ) + f"(global_average)-- apply ma.average along axis = {a} // validate: {fld2.shape}" ) - avg1, sofw = np.ma.average( - fld2, axis=a, weights=wgt, returned=True - ) # sofw is sum of weights + avg1 = np.ma.average(fld2, axis=a, weights=wgt) return np.ma.average(avg1) @@ -719,8 +725,6 @@ def spatial_average(indata, weights=None, spatial_dims=None): Will average over `ncol` if present, and then will check for `lat` and `lon`. When none of those three are found, raise an AdfError. """ - import warnings - if weights is None: # Calculate spatial weights: if "lat" in indata.coords: @@ -802,22 +806,22 @@ def wgt_rmse(fld1, fld2, wgt): fld2 = fld2.compute() if isinstance(fld1, xr.DataArray) and isinstance(fld2, xr.DataArray): return (np.sqrt(((fld1 - fld2) ** 2).weighted(wgt).mean())).values.item() - else: - check = [len(wgt) == s for s in fld1.shape] - if ~np.any(check): - raise IOError( - f"Sorry, weight array has shape {wgt.shape} which is not compatible with data of shape {fld1.shape}" - ) - check = [len(wgt) != s for s in fld1.shape] - dimsize = fld1.shape[ - np.argwhere(check).item() - ] # want to get the dimension length for the dim that does not match the size of wgt - warray = np.tile( - wgt, (dimsize, 1) - ).transpose() # May need more logic to ensure shape is correct. - warray = warray / np.sum(warray) # normalize - wmse = np.sum(warray * (fld1 - fld2) ** 2) - return np.sqrt(wmse).item() + check = [len(wgt) == s for s in fld1.shape] + if ~np.any(check): + raise IOError( + f"Sorry, weight array has shape {wgt.shape} which is not compatible " + f"with data of shape {fld1.shape}" + ) + check = [len(wgt) != s for s in fld1.shape] + dimsize = fld1.shape[ + np.argwhere(check).item() + ] # want to get the dimension length for the dim that does not match the size of wgt + warray = np.tile( + wgt, (dimsize, 1) + ).transpose() # May need more logic to ensure shape is correct. + warray = warray / np.sum(warray) # normalize + wmse = np.sum(warray * (fld1 - fld2) ** 2) + return np.sqrt(wmse).item() ####### @@ -926,37 +930,38 @@ def seasonal_mean(data, season=None, is_climo=None): return data.drop_vars("time") try: - month_length = data.time.dt.days_in_month - except (AttributeError, TypeError): + # Check for a decoded time dimension (the value itself is not needed): + _ = data.time.dt.days_in_month + except (AttributeError, TypeError) as err: # do our best to determine the temporal dimension and assign weights if not is_climo: raise ValueError( "Non-climo file provided, but without a decoded time dimension." - ) - else: - # CLIMO file: try to determine which dimension is month - has_time = False - if isinstance(data, xr.DataArray): - has_time = "time" in data.dims - if not has_time: - if "month" in data.dims: - data = data.rename({"month": "time"}) - has_time = True + ) from err + # CLIMO file: try to determine which dimension is month + has_time = False + if isinstance(data, xr.DataArray): + has_time = "time" in data.dims if not has_time: - # this might happen if a pure numpy array gets passed in - # --> assumes ordered January to December. - assert (12 in data.shape) and ( - data.shape.count(12) == 1 - ), f"Sorry, {data.shape.count(12)} dimensions have size 12, making determination of which dimension is month ambiguous. Please provide a `time` or `month` dimension." - time_dim_num = data.shape.index(12) - fakedims = [f"dim{n}" for n in range(len(data.shape))] - fakedims[time_dim_num] = "time" - data = xr.DataArray(data, dims=fakedims, attrs=data.attrs) - timefix = pd.date_range( - start="1/1/1999", end="12/1/1999", freq="MS" - ) # generic time coordinate from a non-leap-year - data = data.assign_coords({"time": timefix}) - month_length = data.time.dt.days_in_month + if "month" in data.dims: + data = data.rename({"month": "time"}) + has_time = True + if not has_time: + # this might happen if a pure numpy array gets passed in + # --> assumes ordered January to December. + assert (12 in data.shape) and (data.shape.count(12) == 1), ( + f"Sorry, {data.shape.count(12)} dimensions have size 12, making " + "determination of which dimension is month ambiguous. Please " + "provide a `time` or `month` dimension." + ) + time_dim_num = data.shape.index(12) + fakedims = [f"dim{n}" for n in range(len(data.shape))] + fakedims[time_dim_num] = "time" + data = xr.DataArray(data, dims=fakedims, attrs=data.attrs) + timefix = pd.date_range( + start="1/1/1999", end="12/1/1999", freq="MS" + ) # generic time coordinate from a non-leap-year + data = data.assign_coords({"time": timefix}) # End try/except data = data.sel( @@ -1089,8 +1094,16 @@ def vert_remap(x_mdl, p_mdl, plev): ##### +# "P0" is kept to match the CAM variable name, and callers pass it by keyword: +# pylint: disable-next=too-many-positional-arguments def lev_to_plev( - data, ps, hyam, hybm, P0=100000.0, new_levels=None, convert_to_mb=False + data, + ps, + hyam, + hybm, + P0=100000.0, # pylint: disable=invalid-name + new_levels=None, + convert_to_mb=False, ): """Interpolate model hybrid levels to specified pressure levels. @@ -1157,14 +1170,14 @@ def lev_to_plev( lev_new.name = "lev" lev_new.attrs["units"] = "hPa" lev_new.attrs["history"] = ( - f"converted to hPa by dividing by 100 in adf_utils.lev_to_plev" + "converted to hPa by dividing by 100 in adf_utils.lev_to_plev" ) data_interp_rename["lev"] = lev_new data_interp_rename.attrs = attrs else: data_interp_rename.attrs["units"] = "Pa" data_interp_rename.attrs["history"] = ( - f"Interpolated using GeoCAT, assume units of Pa in adf_utils.lev_to_plev" + "Interpolated using GeoCAT, assume units of Pa in adf_utils.lev_to_plev" ) return data_interp_rename @@ -1478,10 +1491,7 @@ def lat_lon_validate_dims(fld): if len(fld.dims) > 3: return False validate = validate_dims(fld, ["lat", "lon"]) - if not all(validate.values()): - return False - else: - return True + return all(validate.values()) def zm_validate_dims(fld): diff --git a/lib/adf_web.py b/lib/adf_web.py index 2f4bb832b..a0d4b1feb 100644 --- a/lib/adf_web.py +++ b/lib/adf_web.py @@ -239,7 +239,7 @@ def _write_run_info_to_web(self, run_info, config_file, active_env): font_18 = "style='font-size:18px;'" font_16 = "style='font-size:16px;'" - with open(run_info, "w") as f: + with open(run_info, "w", encoding="utf-8") as f: # Gather config yaml file info f.write("

") diff --git a/lib/plotting_functions.py b/lib/plotting_functions.py index 522cb79c5..ec33642c8 100644 --- a/lib/plotting_functions.py +++ b/lib/plotting_functions.py @@ -32,8 +32,21 @@ Produce filled contours of fld1, fld2, and their difference with square axes. """ -# import statements: +# This module collects many plotting functions, so allow it to be long: +# pylint: disable=too-many-lines + +# ++++++++++++++++++++++++++++++ +# Import standard python modules +# ++++++++++++++++++++++++++++++ + from typing import Optional + +import warnings # use to warn user about missing files. + +# +++++++++++++++++++++++++++++++++++++++++++++++++ +# import non-standard python modules, including ADF +# +++++++++++++++++++++++++++++++++++++++++++++++++ + import numpy as np import xarray as xr import matplotlib as mpl @@ -50,17 +63,15 @@ import adf_utils as utils # Format warning messages: -import warnings # use to warn user about missing files. - warnings.formatwarning = utils.my_formatwarning # Set non-X-window backend for matplotlib: mpl.use("Agg") -# Now import pyplot: -import matplotlib.pyplot as plt +# Now import pyplot (must come after the backend is set above): +import matplotlib.pyplot as plt # pylint: disable=wrong-import-order,ungrouped-imports -empty_message = "No Valid\nData Points" +EMPTY_MESSAGE = "No Valid\nData Points" props = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9} @@ -73,12 +84,12 @@ "SON": [9, 10, 11], } - ################# # HELPER FUNCTIONS ################# +# pylint: disable-next=too-many-arguments,too-many-positional-arguments def make_polar_plot( wks, case_nickname, @@ -154,7 +165,8 @@ def make_polar_plot( proj = ccrs.SouthPolarStereo() else: raise AdfError( - f"[make_polar_plot] hemisphere not specified, must be NH or SH; hemisphere set as {hemisphere}" + "[make_polar_plot] hemisphere not specified, must be NH or SH; " + f"hemisphere set as {hemisphere}" ) if domain is None: @@ -189,12 +201,8 @@ def make_polar_plot( minval = np.min([np.min(d1), np.min(d2)]) maxval = np.max([np.max(d1), np.max(d2)]) absmaxdif = np.max(np.abs(dif)) - absmaxpct = np.max(np.abs(pct)) - if "colormap" in kwargs: - cmap1 = kwargs["colormap"] - else: - cmap1 = "coolwarm" + cmap1 = kwargs.get("colormap", "coolwarm") if "contour_levels" in kwargs: levels1 = kwargs["contour_levels"] @@ -297,11 +305,7 @@ def make_polar_plot( dnorm, cmapdiff = plot_utils.get_difference_colors(levelsdiff) # Pct Difference options -- Check in kwargs for colormap and levels - if "pct_diff_colormap" in kwargs: - cmappct = kwargs["pct_diff_colormap"] - else: - cmappct = "PuOr_r" - # End if + cmappct = kwargs.get("pct_diff_colormap", "PuOr_r") # -- end options lons, lats = plot_utils.transform_coordinates_for_projection( @@ -323,21 +327,19 @@ def make_polar_plot( # BPM: removing `transform=ccrs.PlateCarree()` from contourf calls & transform_first=True if len(levs) < 2: img1 = ax1.contourf(lons, lats, d1_cyclic, colors="w", norm=norm1) - ax1.text(0.4, 0.4, empty_message, transform=ax1.transAxes, bbox=props) + ax1.text(0.4, 0.4, EMPTY_MESSAGE, transform=ax1.transAxes, bbox=props) - img2 = ax2.contourf(lons, lats, d2_cyclic, colors="w", norm=norm1) - ax2.text(0.4, 0.4, empty_message, transform=ax2.transAxes, bbox=props) + ax2.contourf(lons, lats, d2_cyclic, colors="w", norm=norm1) + ax2.text(0.4, 0.4, EMPTY_MESSAGE, transform=ax2.transAxes, bbox=props) else: img1 = ax1.contourf( lons, lats, d1_cyclic, cmap=cmap1, norm=norm1, levels=levels1 ) - img2 = ax2.contourf( - lons, lats, d2_cyclic, cmap=cmap1, norm=norm1, levels=levels1 - ) + ax2.contourf(lons, lats, d2_cyclic, cmap=cmap1, norm=norm1, levels=levels1) if len(levs_pctdiff) < 2: img3 = ax3.contourf(lons, lats, pct_cyclic, colors="w", norm=pctnorm) - ax3.text(0.4, 0.4, empty_message, transform=ax3.transAxes, bbox=props) + ax3.text(0.4, 0.4, EMPTY_MESSAGE, transform=ax3.transAxes, bbox=props) else: img3 = ax3.contourf( lons, lats, pct_cyclic, cmap=cmappct, norm=pctnorm, levels=levelspctdiff @@ -345,7 +347,7 @@ def make_polar_plot( if len(levs_diff) < 2: img4 = ax4.contourf(lons, lats, dif_cyclic, colors="w", norm=dnorm) - ax4.text(0.4, 0.4, empty_message, transform=ax4.transAxes, bbox=props) + ax4.text(0.4, 0.4, EMPTY_MESSAGE, transform=ax4.transAxes, bbox=props) else: img4 = ax4.contourf( lons, lats, dif_cyclic, cmap=cmapdiff, norm=dnorm, levels=levelsdiff @@ -360,7 +362,7 @@ def make_polar_plot( r"$\mathbf{Test}:$" + f"{case_nickname}\nyears: {case_climo_yrs[0]}-{case_climo_yrs[-1]}" ) - ax1.set_title(case_title, loc="left", fontsize=6) # fontsize=tiFontSize + ax1.set_title(case_title, loc="left", fontsize=6) # fontsize=ti_font_size if obs: obs_var = kwargs["obs_var_name"] @@ -372,7 +374,7 @@ def make_polar_plot( + r"$\mathbf{Variable}:$" + f"{obs_var}" ) - ax2.set_title(base_title, loc="left", fontsize=6) # fontsize=tiFontSize + ax2.set_title(base_title, loc="left", fontsize=6) # fontsize=ti_font_size else: base_title = ( r"$\mathbf{Baseline}:$" @@ -417,8 +419,9 @@ def make_polar_plot( ax2.set_ylabel(f"{d1.units}") ax4.set_ylabel(f"{d1.units}") - [a.set_extent(domain, ccrs.PlateCarree()) for a in [ax1, ax2, ax3, ax4]] - [a.coastlines() for a in [ax1, ax2, ax3, ax4]] + for a in [ax1, ax2, ax3, ax4]: + a.set_extent(domain, ccrs.PlateCarree()) + a.coastlines() # __Follow the cartopy gallery example to make circular__: # Compute a circle in axes coordinates, which we can use as a boundary @@ -428,7 +431,8 @@ def make_polar_plot( center, radius = [0.5, 0.5], 0.5 verts = np.vstack([np.sin(theta), np.cos(theta)]).T circle = mpl.path.Path(verts * radius + center) - [a.set_boundary(circle, transform=a.transAxes) for a in [ax1, ax2, ax3, ax4]] + for a in [ax1, ax2, ax3, ax4]: + a.set_boundary(circle, transform=a.transAxes) # __COLORBARS__ cb_mean_ax = inset_axes( @@ -476,13 +480,15 @@ def make_polar_plot( ####### +# "plev" is unused, but is kept so as to not break existing calls: +# pylint: disable-next=too-many-arguments,too-many-positional-arguments def plot_map_vect_and_save( wks, case_nickname, base_nickname, case_climo_yrs, baseline_climo_yrs, - plev, + plev, # pylint: disable=unused-argument umdlfld_nowrap, vmdlfld_nowrap, uobsfld_nowrap, @@ -574,31 +580,6 @@ def plot_map_vect_and_save( # too many vectors to see well, so prune by striding through data: skip = (slice(None, None, 5), slice(None, None, 8)) - title_string = "Missing title!" - title_string_base = title_string - if "var_name" in kwargs: - var_name = kwargs["var_name"] - else: - var_name = "missing VAR name" - # End if - - if "case_name" in kwargs: - case_name = kwargs["case_name"] - if plev: - title_string = f"{case_name} {var_name} [{plev} hPa]" - else: - title_string = f"{case_name} {var_name}" - # End if - # End if - if "baseline" in kwargs: - data_name = kwargs["baseline"] - if plev: - title_string_base = f"{data_name} {var_name} [{plev} hPa]" - else: - title_string_base = f"{data_name} {var_name}" - # End if - # End if - # Calculate vector magnitudes. # Please note that the difference field needs # to be calculated from the model and obs fields @@ -616,7 +597,7 @@ def plot_map_vect_and_save( max_diff_val = np.max(diff_mag) # Color normalization for difference - if (min_diff_val < 0) and (0 < max_diff_val): + if min_diff_val < 0 < max_diff_val: normdiff = mpl.colors.TwoSlopeNorm( vmin=min_diff_val, vmax=max_diff_val, vcenter=0.0 ) @@ -626,8 +607,9 @@ def plot_map_vect_and_save( # Generate vector plot: # - contourf to show magnitude w/ colorbar - # - vectors (colored or not) to show flow --> subjective (?) choice for how to thin out vectors to be legible - img1 = ax1.contourf( + # - vectors (colored or not) to show flow --> subjective (?) choice for how + # to thin out vectors to be legible + ax1.contourf( lons, lats, mdl_mag, @@ -665,15 +647,9 @@ def plot_map_vect_and_save( # We should think about how to do plot customization and defaults. # Here I'll just pop off a few custom ones, and then pass the rest into mpl. - if "tiString" in kwargs: - tiString = kwargs.pop("tiString") - else: - tiString = "" - if "tiFontSize" in kwargs: - tiFontSize = kwargs.pop("tiFontSize") - else: - tiFontSize = 8 - # End if + # Remove "tiString" so it isn't passed on to matplotlib (it is not used here): + kwargs.pop("tiString", None) + ti_font_size = kwargs.pop("tiFontSize", 8) # Set Main title for subplots: st = fig.suptitle(wks.stem[:-5].replace("_", " - "), fontsize=18) @@ -684,7 +660,7 @@ def plot_map_vect_and_save( r"$\mathbf{Test}:$" + f"{case_nickname}\nyears: {case_climo_yrs[0]}-{case_climo_yrs[-1]}" ) - ax[0].set_title(case_title, loc="left", fontsize=tiFontSize) + ax[0].set_title(case_title, loc="left", fontsize=ti_font_size) if obs: obs_var = kwargs["obs_var_name"] @@ -696,35 +672,38 @@ def plot_map_vect_and_save( + r"$\mathbf{Variable}:$" + f"{obs_var}" ) - ax[1].set_title(base_title, loc="left", fontsize=tiFontSize) + ax[1].set_title(base_title, loc="left", fontsize=ti_font_size) else: base_title = ( r"$\mathbf{Baseline}:$" + f"{base_nickname}\nyears: {baseline_climo_yrs[0]}-{baseline_climo_yrs[-1]}" ) - ax[1].set_title(base_title, loc="left", fontsize=tiFontSize) + ax[1].set_title(base_title, loc="left", fontsize=ti_font_size) # Set stats: area_avg ax[0].set_title( - f"Mean: {mdl_mag.weighted(wgt).mean().item():5.2f}\nMax: {mdl_mag.max():5.2f}\nMin: {mdl_mag.min():5.2f}", + f"Mean: {mdl_mag.weighted(wgt).mean().item():5.2f}\n" + f"Max: {mdl_mag.max():5.2f}\nMin: {mdl_mag.min():5.2f}", loc="right", - fontsize=tiFontSize, + fontsize=ti_font_size, ) ax[1].set_title( - f"Mean: {obs_mag.weighted(wgt).mean().item():5.2f}\nMax: {obs_mag.max():5.2f}\nMin: {obs_mag.min():5.2f}", + f"Mean: {obs_mag.weighted(wgt).mean().item():5.2f}\n" + f"Max: {obs_mag.max():5.2f}\nMin: {obs_mag.min():5.2f}", loc="right", - fontsize=tiFontSize, + fontsize=ti_font_size, ) ax[-1].set_title( - f"Mean: {diff_mag.weighted(wgt).mean().item():5.2f}\nMax: {diff_mag.max():5.2f}\nMin: {diff_mag.min():5.2f}", + f"Mean: {diff_mag.weighted(wgt).mean().item():5.2f}\n" + f"Max: {diff_mag.max():5.2f}\nMin: {diff_mag.min():5.2f}", loc="right", - fontsize=tiFontSize, + fontsize=ti_font_size, ) # set rmse title: - ax[-1].set_title(f"RMSE: ", fontsize=tiFontSize) + ax[-1].set_title("RMSE: ", fontsize=ti_font_size) ax[-1].set_title( - r"$\mathbf{Test} - \mathbf{Baseline}$", loc="left", fontsize=tiFontSize + r"$\mathbf{Test} - \mathbf{Baseline}$", loc="left", fontsize=ti_font_size ) if "units" in kwargs: @@ -797,6 +776,7 @@ def plot_map_vect_and_save( ####### +# pylint: disable-next=too-many-arguments,too-many-positional-arguments def plot_map_and_save( wks, case_nickname, @@ -846,9 +826,11 @@ def plot_map_and_save( - diff_contour_levels - tiString -> str, Title String - tiFontSize -> int, Title Font Size - - mpl -> dict, This should be any matplotlib kwargs that should be passed along. Keep reading: + - mpl -> dict, This should be any matplotlib kwargs that should be passed along. + Keep reading: + Organize these by the mpl function. In this function (`plot_map_and_save`) - we will check for an entry called `subplots`, `contourf`, and `colorbar`. So the YAML might looks something like: + we will check for an entry called `subplots`, `contourf`, and `colorbar`. + So the YAML might looks something like: ``` mpl: subplots: @@ -859,13 +841,12 @@ def plot_map_and_save( colorbar: shrink: 0.4 ``` - + This is experimental, and if you find yourself doing much with this, you probably should write a new plotting script that does not rely on this module. - When these are not provided, colormap is set to 'coolwarm' and limits/levels are set by data range. + + This is experimental, and if you find yourself doing much with this, you + probably should write a new plotting script that does not rely on this module. + When these are not provided, colormap is set to 'coolwarm' and limits/levels are + set by data range. """ - # nice formatting for tick labels - from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter - # preprocess # - assume all three fields have same lat/lon lat = obsfld["lat"] @@ -878,7 +859,8 @@ def plot_map_and_save( # mesh for plots: lons, lats = np.meshgrid(lon, lat) # Note: using wrapped data makes spurious lines across plot (maybe coordinate dependent) - lon2, lat2 = np.meshgrid(mdlfld["lon"], mdlfld["lat"]) + # (Only needed for the contour lines below, which are currently turned off): + # lon2, lat2 = np.meshgrid(mdlfld["lon"], mdlfld["lat"]) # get statistics (from non-wrapped) fields = (mdlfld, obsfld, diffld, pctld) @@ -892,17 +874,9 @@ def plot_map_and_save( # We should think about how to do plot customization and defaults. # Here I'll just pop off a few custom ones, and then pass the rest into mpl. - if "tiString" in kwargs: - tiString = kwargs.pop("tiString") - else: - tiString = "" - # End if - - if "tiFontSize" in kwargs: - tiFontSize = kwargs.pop("tiFontSize") - else: - tiFontSize = 8 - # End if + # Remove "tiString" so it isn't passed on to matplotlib (it is not used here): + kwargs.pop("tiString", None) + ti_font_size = kwargs.pop("tiFontSize", 8) # generate dictionary of contour plot settings: cp_info = plot_utils.prep_contour_plot(mdlfld, obsfld, diffld, pctld, **kwargs) @@ -925,8 +899,7 @@ def plot_map_and_save( ax = [ax1, ax2, ax3, ax4] img = [] # contour plots - cs = [] # contour lines - cb = [] # color bars + # cs = [] # contour lines (currently turned off, see below) # formatting for tick labels lon_formatter = LongitudeFormatter( @@ -961,7 +934,7 @@ def plot_map_and_save( transform_first=True, ) ) - ax[i].text(0.4, 0.4, empty_message, transform=ax[i].transAxes, bbox=props) + ax[i].text(0.4, 0.4, EMPTY_MESSAGE, transform=ax[i].transAxes, bbox=props) else: img.append( ax[i].contourf( @@ -977,14 +950,17 @@ def plot_map_and_save( ) ) # End if - ax[i].set_title("AVG: {0:.3f}".format(area_avg[i]), loc="right", fontsize=11) + ax[i].set_title(f"AVG: {area_avg[i]:.3f}", loc="right", fontsize=11) # add contour lines <- Unused for now -JN - # TODO: add an option to turn this on -BM - # cs.append(ax[i].contour(lon2, lat2, fields[i], transform=ccrs.PlateCarree(), colors='k', linewidths=1)) - # ax[i].clabel(cs[i], cs[i].levels, inline=True, fontsize=tiFontSize-2, fmt='%1.1f') - # ax[i].text( 10, -140, "CONTOUR FROM {} to {} by {}".format(min(cs[i].levels), max(cs[i].levels), cs[i].levels[1]-cs[i].levels[0]), - # bbox=dict(facecolor='none', edgecolor='black'), fontsize=tiFontSize-2) + # TODO: add an option to turn this on -BM # pylint: disable=fixme + # cs.append(ax[i].contour(lon2, lat2, fields[i], transform=ccrs.PlateCarree(), + # colors='k', linewidths=1)) + # ax[i].clabel(cs[i], cs[i].levels, inline=True, fontsize=ti_font_size-2, + # fmt='%1.1f') + # ax[i].text( 10, -140, "CONTOUR FROM {} to {} by {}".format(min(cs[i].levels), + # max(cs[i].levels), cs[i].levels[1]-cs[i].levels[0]), + # bbox=dict(facecolor='none', edgecolor='black'), fontsize=ti_font_size-2) st = fig.suptitle(wks.stem[:-5].replace("_", " - "), fontsize=18) st.set_y(0.85) @@ -994,7 +970,7 @@ def plot_map_and_save( r"$\mathbf{Test}:$" + f"{case_nickname}\nyears: {case_climo_yrs[0]}-{case_climo_yrs[-1]}" ) - ax[0].set_title(case_title, loc="left", fontsize=tiFontSize) + ax[0].set_title(case_title, loc="left", fontsize=ti_font_size) if obs: obs_var = kwargs["obs_var_name"] @@ -1006,43 +982,47 @@ def plot_map_and_save( + r"$\mathbf{Variable}:$" + f"{obs_var}" ) - ax[1].set_title(base_title, loc="left", fontsize=tiFontSize) + ax[1].set_title(base_title, loc="left", fontsize=ti_font_size) else: base_title = ( r"$\mathbf{Baseline}:$" + f"{base_nickname}\nyears: {baseline_climo_yrs[0]}-{baseline_climo_yrs[-1]}" ) - ax[1].set_title(base_title, loc="left", fontsize=tiFontSize) + ax[1].set_title(base_title, loc="left", fontsize=ti_font_size) # Set stats: area_avg ax[0].set_title( - f"Mean: {mdlfld.weighted(wgt).mean().item():5.2f}\nMax: {mdlfld.max():5.2f}\nMin: {mdlfld.min():5.2f}", + f"Mean: {mdlfld.weighted(wgt).mean().item():5.2f}\n" + f"Max: {mdlfld.max():5.2f}\nMin: {mdlfld.min():5.2f}", loc="right", - fontsize=tiFontSize, + fontsize=ti_font_size, ) ax[1].set_title( - f"Mean: {obsfld.weighted(wgt).mean().item():5.2f}\nMax: {obsfld.max():5.2f}\nMin: {obsfld.min():5.2f}", + f"Mean: {obsfld.weighted(wgt).mean().item():5.2f}\n" + f"Max: {obsfld.max():5.2f}\nMin: {obsfld.min():5.2f}", loc="right", - fontsize=tiFontSize, + fontsize=ti_font_size, ) ax[2].set_title( - f"Mean: {pctld.weighted(wgt).mean().item():5.2f}\nMax: {pctld.max():5.2f}\nMin: {pctld.min():5.2f}", + f"Mean: {pctld.weighted(wgt).mean().item():5.2f}\n" + f"Max: {pctld.max():5.2f}\nMin: {pctld.min():5.2f}", loc="right", - fontsize=tiFontSize, + fontsize=ti_font_size, ) ax[3].set_title( - f"Mean: {diffld.weighted(wgt).mean().item():5.2f}\nMax: {diffld.max():5.2f}\nMin: {diffld.min():5.2f}", + f"Mean: {diffld.weighted(wgt).mean().item():5.2f}\n" + f"Max: {diffld.max():5.2f}\nMin: {diffld.min():5.2f}", loc="right", - fontsize=tiFontSize, + fontsize=ti_font_size, ) # set rmse title: - ax[3].set_title(f"RMSE: {d_rmse:.3f}", fontsize=tiFontSize) + ax[3].set_title(f"RMSE: {d_rmse:.3f}", fontsize=ti_font_size) ax[3].set_title( - r"$\mathbf{Test} - \mathbf{Baseline}$", loc="left", fontsize=tiFontSize + r"$\mathbf{Test} - \mathbf{Baseline}$", loc="left", fontsize=ti_font_size ) ax[2].set_title( - "Test % Diff Baseline", loc="left", fontsize=tiFontSize, fontweight="bold" + "Test % Diff Baseline", loc="left", fontsize=ti_font_size, fontweight="bold" ) for a in ax: @@ -1076,8 +1056,8 @@ def plot_map_and_save( bbox_transform=ax3.transAxes, borderpad=0, ) - PCT_CB = fig.colorbar(img[2], cax=cb_pct_ax, **cp_info["colorbar_opt"]) - PCT_CB.ax.set_ylabel = "%" + pct_cb = fig.colorbar(img[2], cax=cb_pct_ax, **cp_info["colorbar_opt"]) + pct_cb.ax.set_ylabel = "%" cb_diff_ax = inset_axes( ax4, @@ -1129,9 +1109,8 @@ def zonal_plot(lat, data, ax=None, color=None, **kwargs): if "lev" in data.dims: img, ax = plot_utils.zonal_plot_preslat(ax, lat, data["lev"], data, **kwargs) return img, ax - else: - ax = plot_utils.zonal_plot_line(ax, lat, data, color, **kwargs) - return ax + ax = plot_utils.zonal_plot_line(ax, lat, data, color, **kwargs) + return ax def meridional_plot(lon, data, ax=None, color=None, **kwargs): @@ -1166,14 +1145,14 @@ def meridional_plot(lon, data, ax=None, color=None, **kwargs): ax, lon, data["lev"], data, **kwargs ) return img, ax - else: - ax = plot_utils.meridional_plot_line(ax, lon, data, color, **kwargs) - return ax + ax = plot_utils.meridional_plot_line(ax, lon, data, color, **kwargs) + return ax ####### +# pylint: disable-next=too-many-arguments,too-many-positional-arguments def plot_zonal_mean_and_save( wks, case_nickname, @@ -1201,16 +1180,19 @@ def plot_zonal_mean_and_save( + 3 panels: (top) zonal mean adata, (middle) zonal mean bdata, (bottom) difference + pcolormesh/contour plot kwargs -> optional dictionary of plotting options - ** Expecting this to be variable-specific section, possibly provided by ADF Variable Defaults YAML file.** + ** Expecting this to be variable-specific section, possibly provided by + ADF Variable Defaults YAML file.** - colormap -> str, name of matplotlib colormap - contour_levels -> list of explict values or a tuple: (min, max, step) - diff_colormap - diff_contour_levels - tiString -> str, Title String - tiFontSize -> int, Title Font Size - - mpl -> dict, This should be any matplotlib kwargs that should be passed along. Keep reading: + - mpl -> dict, This should be any matplotlib kwargs that should be passed along. + Keep reading: + Organize these by the mpl function. In this function (`plot_map_and_save`) - we will check for an entry called `subplots`, `contourf`, and `colorbar`. So the YAML might looks something like: + we will check for an entry called `subplots`, `contourf`, and `colorbar`. + So the YAML might looks something like: ``` mpl: subplots: @@ -1226,11 +1208,7 @@ def plot_zonal_mean_and_save( # style the plot: # We should think about how to do plot customization and defaults. # Here I'll just pop off a few custom ones, and then pass the rest into mpl. - if "tiFontSize" in kwargs: - tiFontSize = kwargs.pop("tiFontSize") - else: - tiFontSize = 8 - # End if + ti_font_size = kwargs.pop("tiFontSize", 8) # Set plot titles case_title = ( @@ -1287,9 +1265,9 @@ def plot_zonal_mean_and_save( if len(levs) < 2: img0, ax[0] = zonal_plot(adata["lat"], azm, ax=ax[0]) - ax[0].text(0.4, 0.4, empty_message, transform=ax[0].transAxes, bbox=props) + ax[0].text(0.4, 0.4, EMPTY_MESSAGE, transform=ax[0].transAxes, bbox=props) img1, ax[1] = zonal_plot(bdata["lat"], bzm, ax=ax[1]) - ax[1].text(0.4, 0.4, empty_message, transform=ax[1].transAxes, bbox=props) + ax[1].text(0.4, 0.4, EMPTY_MESSAGE, transform=ax[1].transAxes, bbox=props) else: img0, ax[0] = zonal_plot( adata["lat"], @@ -1315,7 +1293,7 @@ def plot_zonal_mean_and_save( if len(levs_diff) < 2: img2, ax[2] = zonal_plot(adata["lat"], diff, ax=ax[2]) - ax[2].text(0.4, 0.4, empty_message, transform=ax[2].transAxes, bbox=props) + ax[2].text(0.4, 0.4, EMPTY_MESSAGE, transform=ax[2].transAxes, bbox=props) else: img2, ax[2] = zonal_plot( adata["lat"], @@ -1332,7 +1310,7 @@ def plot_zonal_mean_and_save( if len(levs_pct_diff) < 2: img3, ax[3] = zonal_plot(adata["lat"], pct, ax=ax[3]) - ax[3].text(0.4, 0.4, empty_message, transform=ax[3].transAxes, bbox=props) + ax[3].text(0.4, 0.4, EMPTY_MESSAGE, transform=ax[3].transAxes, bbox=props) else: img3, ax[3] = zonal_plot( adata["lat"], @@ -1347,13 +1325,13 @@ def plot_zonal_mean_and_save( img3, ax=ax[3], location="right", **cp_info["pct_colorbar_opt"] ) - ax[0].set_title(case_title, loc="left", fontsize=tiFontSize) - ax[1].set_title(base_title, loc="left", fontsize=tiFontSize) + ax[0].set_title(case_title, loc="left", fontsize=ti_font_size) + ax[1].set_title(base_title, loc="left", fontsize=ti_font_size) ax[2].set_title( - r"$\mathbf{Test} - \mathbf{Baseline}$", loc="left", fontsize=tiFontSize + r"$\mathbf{Test} - \mathbf{Baseline}$", loc="left", fontsize=ti_font_size ) ax[3].set_title( - "Test % Diff Baseline", loc="left", fontsize=tiFontSize, fontweight="bold" + "Test % Diff Baseline", loc="left", fontsize=ti_font_size, fontweight="bold" ) # style the plot: @@ -1363,7 +1341,8 @@ def plot_zonal_mean_and_save( ax[-1].set_xlabel("LATITUDE") if log_p: - [a.set_yscale("log") for a in ax] + for a in ax: + a.set_yscale("log") fig.text(-0.03, 0.5, "PRESSURE [hPa]", va="center", rotation="vertical") else: @@ -1423,7 +1402,8 @@ def plot_zonal_mean_and_save( for a in ax: try: a.label_outer() - except: + # Labeling is cosmetic, so never let it stop the plot from being saved: + except Exception: # pylint: disable=broad-exception-caught pass # End except # End for @@ -1439,6 +1419,8 @@ def plot_zonal_mean_and_save( ####### +# "log_p" is unused, but is kept so as to not break existing calls: +# pylint: disable-next=too-many-arguments,too-many-positional-arguments def plot_meridional_mean_and_save( wks, case_nickname, @@ -1448,7 +1430,7 @@ def plot_meridional_mean_and_save( adata, bdata, has_lev, - log_p=False, + log_p=False, # pylint: disable=unused-argument latbounds=None, obs=False, **kwargs, @@ -1503,8 +1485,10 @@ def plot_meridional_mean_and_save( - diff_contour_levels -> list of explicit values or a tuple: (min, max, step) - tiString -> str, Title String - tiFontSize -> int, Title Font Size - - mpl -> dict, This should be any matplotlib kwargs that should be passed along. Keep reading: - + Organize these by the mpl function. In this function (`plot_meridional_mean_and_save`) + - mpl -> dict, This should be any matplotlib kwargs that should be passed along. + Keep reading: + + Organize these by the mpl function. In this function + (`plot_meridional_mean_and_save`) we will check for an entry called `subplots`, `contourf`, and `colorbar`. So the YAML might looks something like: ``` @@ -1529,7 +1513,8 @@ def plot_meridional_mean_and_save( latbounds, slice ): # If not a slice object, then quit this routine. print( - f"ERROR: plot_meridonal_mean_and_save - received an invalid value for latbounds ({latbounds}). Must be a number or a slice." + "ERROR: plot_meridonal_mean_and_save - received an invalid value for " + f"latbounds ({latbounds}). Must be a number or a slice." ) return None # End if @@ -1537,11 +1522,7 @@ def plot_meridional_mean_and_save( # style the plot: # We should think about how to do plot customization and defaults. # Here I'll just pop off a few custom ones, and then pass the rest into mpl. - if "tiFontSize" in kwargs: - tiFontSize = kwargs.pop("tiFontSize") - else: - tiFontSize = 8 - # End if + ti_font_size = kwargs.pop("tiFontSize", 8) # possible that the data has time, but usually it won't if len(adata.dims) > 4: @@ -1567,7 +1548,8 @@ def plot_meridional_mean_and_save( # If there are other dimensions, they are still going to be there: if len(adata.dims) > 2: print( - f"ERROR: plot_meridonal_mean_and_save - AFTER averaging, there are too many dimensions: {adata.dims}" + "ERROR: plot_meridonal_mean_and_save - AFTER averaging, there are too " + f"many dimensions: {adata.dims}" ) return None @@ -1581,7 +1563,9 @@ def plot_meridional_mean_and_save( # plot-controlling parameters: xdim = "lon" # the name used for the x-axis dimension - pltfunc = meridional_plot # the plotting function ... maybe we can generalize to get zonal/meridional into one function (?) + # the plotting function ... maybe we can generalize to get zonal/meridional + # into one function (?) + pltfunc = meridional_plot case_title = ( r"$\mathbf{Test}:$" @@ -1623,9 +1607,9 @@ def plot_meridional_mean_and_save( if len(levs) < 2: img0, ax[0] = pltfunc(adata[xdim], adata, ax=ax[0]) - ax[0].text(0.4, 0.4, empty_message, transform=ax[0].transAxes, bbox=props) + ax[0].text(0.4, 0.4, EMPTY_MESSAGE, transform=ax[0].transAxes, bbox=props) img1, ax[1] = pltfunc(bdata[xdim], bdata, ax=ax[1]) - ax[1].text(0.4, 0.4, empty_message, transform=ax[1].transAxes, bbox=props) + ax[1].text(0.4, 0.4, EMPTY_MESSAGE, transform=ax[1].transAxes, bbox=props) else: img0, ax[0] = pltfunc( adata[xdim], @@ -1645,17 +1629,13 @@ def plot_meridional_mean_and_save( levels=cp_info["levels1"], **cp_info["contourf_opt"], ) - cb0 = fig.colorbar( - img0, ax=ax[0], location="right", **cp_info["colorbar_opt"] - ) - cb1 = fig.colorbar( - img1, ax=ax[1], location="right", **cp_info["colorbar_opt"] - ) + fig.colorbar(img0, ax=ax[0], location="right", **cp_info["colorbar_opt"]) + fig.colorbar(img1, ax=ax[1], location="right", **cp_info["colorbar_opt"]) # End if if len(levs_diff) < 2: img2, ax[2] = pltfunc(adata[xdim], diff, ax=ax[2]) - ax[2].text(0.4, 0.4, empty_message, transform=ax[2].transAxes, bbox=props) + ax[2].text(0.4, 0.4, EMPTY_MESSAGE, transform=ax[2].transAxes, bbox=props) else: img2, ax[2] = pltfunc( adata[xdim], @@ -1666,13 +1646,11 @@ def plot_meridional_mean_and_save( levels=cp_info["levelsdiff"], **cp_info["contourf_opt"], ) - cb2 = fig.colorbar( - img2, ax=ax[2], location="right", **cp_info["colorbar_opt"] - ) + fig.colorbar(img2, ax=ax[2], location="right", **cp_info["colorbar_opt"]) if len(levs_pctdiff) < 2: img3, ax[3] = pltfunc(adata[xdim], pct, ax=ax[3]) - ax[3].text(0.4, 0.4, empty_message, transform=ax[3].transAxes, bbox=props) + ax[3].text(0.4, 0.4, EMPTY_MESSAGE, transform=ax[3].transAxes, bbox=props) else: img3, ax[3] = pltfunc( adata[xdim], @@ -1683,18 +1661,16 @@ def plot_meridional_mean_and_save( levels=cp_info["levelspctdiff"], **cp_info["contourf_opt"], ) - cb3 = fig.colorbar( - img3, ax=ax[3], location="right", **cp_info["colorbar_opt"] - ) + fig.colorbar(img3, ax=ax[3], location="right", **cp_info["colorbar_opt"]) # Set plot titles - ax[0].set_title(case_title, loc="left", fontsize=tiFontSize) - ax[1].set_title(base_title, loc="left", fontsize=tiFontSize) + ax[0].set_title(case_title, loc="left", fontsize=ti_font_size) + ax[1].set_title(base_title, loc="left", fontsize=ti_font_size) ax[2].set_title( - r"$\mathbf{Test} - \mathbf{Baseline}$", loc="left", fontsize=tiFontSize + r"$\mathbf{Test} - \mathbf{Baseline}$", loc="left", fontsize=ti_font_size ) ax[3].set_title( - "Test % Diff Baseline", loc="left", fontsize=tiFontSize, fontweight="bold" + "Test % Diff Baseline", loc="left", fontsize=ti_font_size, fontweight="bold" ) # style the plot: @@ -1703,7 +1679,8 @@ def plot_meridional_mean_and_save( st.set_y(0.85) ax[-1].set_xlabel("LONGITUDE") if cp_info["plot_log_p"]: - [a.set_yscale("log") for a in ax] + for a in ax: + a.set_yscale("log") fig.text(-0.03, 0.5, "PRESSURE [hPa]", va="center", rotation="vertical") else: @@ -1752,7 +1729,8 @@ def plot_meridional_mean_and_save( for a in ax: try: a.label_outer() - except: + # Labeling is cosmetic, so never let it stop the plot from being saved: + except Exception: # pylint: disable=broad-exception-caught pass # End except # End for @@ -1764,6 +1742,8 @@ def plot_meridional_mean_and_save( # Close plots: plt.close() + return None + ####### @@ -1849,11 +1829,6 @@ def square_contour_difference(fld1, fld2, **kwargs): cbax_top = plt.subplot(grid[0:2, -1]) cbax_bot = plt.subplot(grid[-1, 1:3]) - # determine color normalization for means: - mx = np.max([fld1.max(), fld2.max()]) - mn = np.min([fld1.min(), fld2.min()]) - mnorm = mpl.colors.Normalize(mn, mx) - coord1, coord2 = fld1.coords # ASSUMES xarray WITH coords AND 2-dimensions xx, yy = np.meshgrid(fld1[coord2], fld1[coord1]) @@ -1887,13 +1862,13 @@ def square_contour_difference(fld1, fld2, **kwargs): dmax = diff.max() if dmin > 0: dnorm = mpl.colors.Normalize(dmin, dmax) - cmap = mpl.cm.OrRd + cmap = mpl.colormaps["OrRd"] elif dmax < 0: dnorm = mpl.colors.Normalize(dmin, dmax) - cmap = mpl.cm.BuPu_r + cmap = mpl.colormaps["BuPu_r"] else: dnorm = mpl.colors.TwoSlopeNorm(vmin=dmin, vcenter=0, vmax=dmax) - cmap = mpl.cm.RdBu_r + cmap = mpl.colormaps["RdBu_r"] img3 = ax3.contourf(xx, yy, diff.transpose(), cmap=cmap, norm=dnorm) if (coord1 == "month") and (fld1.shape[0] == 12): @@ -1910,13 +1885,13 @@ def square_contour_difference(fld1, fld2, **kwargs): pmax = pct.max() if pmin > 0: pnorm = mpl.colors.Normalize(pmin, pmax) - cmap = mpl.cm.OrRd + cmap = mpl.colormaps["OrRd"] elif pmax < 0: pnorm = mpl.colors.Normalize(pmin, pmax) - cmap = mpl.cm.BuPu_r + cmap = mpl.colormaps["BuPu_r"] else: pnorm = mpl.colors.TwoSlopeNorm(vmin=pmin, vcenter=0, vmax=pmax) - cmap = mpl.cm.RdBu_r + cmap = mpl.colormaps["RdBu_r"] img4 = ax4.contourf(xx, yy, pct.transpose(), cmap=cmap, norm=pnorm) if (coord1 == "month") and (fld1.shape[0] == 12): @@ -1951,9 +1926,9 @@ def square_contour_difference(fld1, fld2, **kwargs): fig.suptitle(tstr, fontsize=18) - cb1 = fig.colorbar(img1, cax=cbax_top) - cb2 = fig.colorbar(img3, cax=cbax_bot, orientation="horizontal") - cb3 = fig.colorbar(img4, cax=cbax_bot, orientation="horizontal") + fig.colorbar(img1, cax=cbax_top) + fig.colorbar(img3, cax=cbax_bot, orientation="horizontal") + fig.colorbar(img4, cax=cbax_bot, orientation="horizontal") return fig diff --git a/lib/plotting_utils.py b/lib/plotting_utils.py index 19d975049..fbea0af13 100644 --- a/lib/plotting_utils.py +++ b/lib/plotting_utils.py @@ -27,8 +27,16 @@ Create a generic line plot """ -# import statements: +# ++++++++++++++++++++++++++++++ +# Import standard python modules +# ++++++++++++++++++++++++++++++ + from pathlib import Path +import warnings + +# +++++++++++++++++++++++++++++++++++++++++++++++++ +# import non-standard python modules, including ADF +# +++++++++++++++++++++++++++++++++++++++++++++++++ import numpy as np import xarray as xr @@ -38,8 +46,7 @@ from adf_diag import AdfDiag import adf_utils as utils -import warnings # use to warn user about missing files. - +# use to warn user about missing files. warnings.formatwarning = utils.my_formatwarning ################# @@ -49,7 +56,8 @@ def load_dataset(fils): """ - This method exists to get an xarray Dataset from input file information that can be passed into the plotting methods. + This method exists to get an xarray Dataset from input file information that + can be passed into the plotting methods. Parameters ---------- @@ -65,13 +73,11 @@ def load_dataset(fils): When just one entry is provided, use `open_dataset`, otherwise `open_mfdatset` """ if len(fils) == 0: - warnings.warn(f"\t WARNING: Input file list is empty.") + warnings.warn("\t WARNING: Input file list is empty.") return None - elif len(fils) > 1: + if len(fils) > 1: return xr.open_mfdataset(fils, combine="by_coords") - else: - return xr.open_dataset(fils[0]) - # End if + return xr.open_dataset(fils[0]) # End def @@ -119,11 +125,10 @@ def use_this_norm(): mplversion = [int(x) for x in mpl.__version__.split(".")] if mplversion[0] < 3: return mpl.colors.Normalize, mplversion[0] - else: - if mplversion[1] < 2: - return mpl.colors.DivergingNorm, mplversion[0] - else: - return mpl.colors.TwoSlopeNorm, mplversion[0] + if mplversion[1] < 2: + # Only exists in old matplotlib versions: + return mpl.colors.DivergingNorm, mplversion[0] # pylint: disable=no-member + return mpl.colors.TwoSlopeNorm, mplversion[0] ####### @@ -185,7 +190,7 @@ def get_difference_colors(values): Uses 'OrRd' colormap for positive definite, 'BuPu_r' for negative definite, and 'RdBu_r' centered on zero if there are values of both signs. """ - normfunc, mplv = use_this_norm() + normfunc, _ = use_this_norm() finite = np.asarray(values, dtype=float) finite = finite[np.isfinite(finite)] if finite.size == 0: @@ -195,19 +200,19 @@ def get_difference_colors(values): # left 'cmap' unset here and died with an UnboundLocalError. Note # np.min/np.max were used before, so a single missing point anywhere in # the field was enough to reach this. - return mpl.colors.Normalize(vmin=-1.0, vmax=1.0), mpl.cm.RdBu_r + return mpl.colors.Normalize(vmin=-1.0, vmax=1.0), mpl.colormaps["RdBu_r"] dmin = finite.min() dmax = finite.max() # color normalization for difference - cmap = mpl.cm.RdBu_r - if (dmin < 0) and (0 < dmax): + cmap = mpl.colormaps["RdBu_r"] + if dmin < 0 < dmax: dnorm = normfunc(vmin=dmin, vmax=dmax, vcenter=0.0) else: dnorm = mpl.colors.Normalize(vmin=dmin, vmax=dmax) if dmin >= 0: - cmap = mpl.cm.OrRd + cmap = mpl.colormaps["OrRd"] elif dmax <= 0: - cmap = mpl.cm.BuPu_r + cmap = mpl.colormaps["BuPu_r"] return dnorm, cmap @@ -234,7 +239,8 @@ def get_central_longitude(*args): Notes ----- - This allows a script to, for example, allow a config file to specify, but also have a preference: + This allows a script to, for example, allow a config file to specify, but also + have a preference: `get_central_longitude(AdfObj, 30.0)` """ chk_for_adf = [isinstance(arg, AdfDiag) for arg in args] @@ -243,42 +249,36 @@ def get_central_longitude(*args): for arg in args: if isinstance(arg, AdfDiag): result = arg.get_basic_info("central_longitude", required=False) - if ( - (isinstance(result, int) or isinstance(result, float)) - and (result >= -180) - and (result <= 360) - ): + if isinstance(result, (int, float)) and -180 <= result <= 360: return result - else: - # If result exists, then write info to debug log: - if result: - msg = f"central_lngitude of type '{type(result).__name__}'" - msg += f" and value '{result}', which is not a valid longitude" - msg += " for the ADF." - arg.debug_log(msg) - # End if - - # There is only one ADF object per ADF run, so if its - # not present or configured correctly then no - # reason to keep looking: - break # End if + + # If result exists, then write info to debug log: + if result: + msg = f"central_lngitude of type '{type(result).__name__}'" + msg += f" and value '{result}', which is not a valid longitude" + msg += " for the ADF." + arg.debug_log(msg) + # End if + + # There is only one ADF object per ADF run, so if its + # not present or configured correctly then no + # reason to keep looking: + break # End if # End for # End if # 2nd pass through arguments, look for numbers: for arg in args: - if (isinstance(arg, float) or isinstance(arg, int)) and ( - (arg >= -180) and (arg <= 360) - ): + if isinstance(arg, (float, int)) and -180 <= arg <= 360: return arg # End if - else: - # this is the `else` on the for loop --> if non of the arguments meet the criteria, do this. - print("No valid central longitude specified. Defaults to 180.") - return 180 - # End if + # End for + + # None of the arguments meet the criteria: + print("No valid central longitude specified. Defaults to 180.") + return 180 ####### @@ -291,16 +291,16 @@ def get_central_longitude(*args): def _plot_line(axobject, xdata, ydata, color, **kwargs): """Create a generic line plot and check for some ways to annotate.""" - if color != None: + if color is not None: axobject.plot(xdata, ydata, c=color, **kwargs) else: axobject.plot(xdata, ydata, **kwargs) # Set Y-axis label: if hasattr(ydata, "units"): - axobject.set_ylabel("[{units}]".format(units=getattr(ydata, "units"))) + axobject.set_ylabel(f"[{getattr(ydata, 'units')}]") elif "units" in kwargs: - axobject.set_ylabel("[{units}]".format(kwargs["units"])) + axobject.set_ylabel(f"[{kwargs['units']}]") # End if return axobject @@ -316,9 +316,9 @@ def meridional_plot_line(ax, lon, data, color, **kwargs): # ax.set_xlabel("LONGITUDE") if hasattr(data, "units"): - ax.set_ylabel("{units}".format(units=getattr(data, "units"))) + ax.set_ylabel(f"{getattr(data, 'units')}") elif "units" in kwargs: - ax.set_ylabel("{units}".format(kwargs["units"])) + ax.set_ylabel(f"{kwargs['units']}") return ax @@ -331,9 +331,9 @@ def zonal_plot_line(ax, lat, data, color, **kwargs): # ax.set_xlabel("LATITUDE") if hasattr(data, "units"): - ax.set_ylabel("{units}".format(units=getattr(data, "units"))) + ax.set_ylabel(f"{getattr(data, 'units')}") elif "units" in kwargs: - ax.set_ylabel("{units}".format(kwargs["units"])) + ax.set_ylabel(f"{kwargs['units']}") return ax @@ -395,6 +395,8 @@ def colormap_object(cmap): return cmap +# "pctdata" is unused, but is kept so as to not break existing calls: +# pylint: disable-next=unused-argument def prep_contour_plot(adata, bdata, diffdata, pctdata, **kwargs): """Preparation for making contour plots. @@ -437,11 +439,7 @@ def prep_contour_plot(adata, bdata, diffdata, pctdata, **kwargs): # determine norm to use (deprecate this once minimum MPL version is high enough) normfunc, mplv = use_this_norm() - if "colormap" in kwargs: - cmap1 = kwargs["colormap"] - else: - cmap1 = "coolwarm" - # End if + cmap1 = kwargs.get("colormap", "coolwarm") if "contour_levels" in kwargs: levels1 = kwargs["contour_levels"] @@ -479,7 +477,7 @@ def prep_contour_plot(adata, bdata, diffdata, pctdata, **kwargs): # End if if ("colormap" not in kwargs) and ("contour_levels" not in kwargs): - if ((minval < 0) and (0 < maxval)) and mplv > 2: + if (minval < 0 < maxval) and mplv > 2: norm1 = normfunc(vmin=minval, vmax=maxval, vcenter=0.0) else: norm1 = mpl.colors.Normalize(vmin=minval, vmax=maxval) @@ -487,11 +485,7 @@ def prep_contour_plot(adata, bdata, diffdata, pctdata, **kwargs): # End if # Difference options -- Check in kwargs for colormap and levels - if "diff_colormap" in kwargs: - cmapdiff = kwargs["diff_colormap"] - else: - cmapdiff = "coolwarm" - # End if + cmapdiff = kwargs.get("diff_colormap", "coolwarm") if "diff_contour_levels" in kwargs: levelsdiff = kwargs["diff_contour_levels"] # a list of explicit contour levels @@ -507,11 +501,7 @@ def prep_contour_plot(adata, bdata, diffdata, pctdata, **kwargs): # set levels for difference plot: levelsdiff = np.linspace(-1 * absmaxdif, absmaxdif, 12) # Percent Difference options -- Check in kwargs for colormap and levels - if "pct_diff_colormap" in kwargs: - cmappct = kwargs["pct_diff_colormap"] - else: - cmappct = "PuOr_r" - # End if + cmappct = kwargs.get("pct_diff_colormap", "PuOr_r") if "pct_diff_contour_levels" in kwargs: levelspctdiff = kwargs[ @@ -550,13 +540,10 @@ def prep_contour_plot(adata, bdata, diffdata, pctdata, **kwargs): ] pctnorm = mpl.colors.BoundaryNorm(levelspctdiff, 256) - if "plot_log_pressure" in kwargs: - plot_log_p = kwargs["plot_log_pressure"] - else: - plot_log_p = False + plot_log_p = kwargs.get("plot_log_pressure", False) # color normalization for difference - if ((np.min(levelsdiff) < 0) and (0 < np.max(levelsdiff))) and mplv > 2: + if (np.min(levelsdiff) < 0 < np.max(levelsdiff)) and mplv > 2: normdiff = normfunc( vmin=np.min(levelsdiff), vmax=np.max(levelsdiff), vcenter=0.0 ) diff --git a/lib/test/pylintrc b/lib/test/pylintrc index b8e7b54c3..74a694068 100644 --- a/lib/test/pylintrc +++ b/lib/test/pylintrc @@ -36,10 +36,6 @@ load-plugins= # Pickle collected data for later comparisons. persistent=yes -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no @@ -515,6 +511,6 @@ valid-metaclass-classmethod-first-arg=cls [EXCEPTIONS] # Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception +# "builtins.BaseException, builtins.Exception". +overgeneral-exceptions=builtins.BaseException, + builtins.Exception