diff --git a/docs/Users_Guide/figure/apcp_example_mode_objects.png b/docs/Users_Guide/figure/apcp_example_mode_objects.png new file mode 100644 index 000000000..f45cf90a1 Binary files /dev/null and b/docs/Users_Guide/figure/apcp_example_mode_objects.png differ diff --git a/docs/Users_Guide/figure/apcp_example_mode_raw.png b/docs/Users_Guide/figure/apcp_example_mode_raw.png new file mode 100644 index 000000000..4eb48ca63 Binary files /dev/null and b/docs/Users_Guide/figure/apcp_example_mode_raw.png differ diff --git a/docs/Users_Guide/index.rst b/docs/Users_Guide/index.rst index 79c269468..89e328863 100644 --- a/docs/Users_Guide/index.rst +++ b/docs/Users_Guide/index.rst @@ -85,6 +85,7 @@ Atmospheric Research (NCAR), sponsored by National Science Foundation. hovmoeller spacetime make_maki_enso + mode_field_plot mjo_rmm_omi taylor_diagram tcrmw_cross_section diff --git a/docs/Users_Guide/mode_field_plot.rst b/docs/Users_Guide/mode_field_plot.rst new file mode 100644 index 000000000..16f4742ef --- /dev/null +++ b/docs/Users_Guide/mode_field_plot.rst @@ -0,0 +1,403 @@ +*************** +Mode Field Plot +*************** + +Description +=========== +The mode field plot replaces the deprecated MET MODE plotting utility plot_mode_field. + +Example +======= + +Sample Data +----------- + +The netCDF object file generated by the MET MODE tool is required input to the plot. + +The sample data for creating the example plot examples described below is available in the source code, under the METplotpy/test/mode_field_plot directory: + + + .. code-block:: + + $METPLOTPY_BASE/test/mode_field_plot/mode_KWBC_APCP_24_vs_IMERG_APCP_24_960000L_20240207_000000V_240000A_R1_T2_obj.nc + + replace *$METPLOTPY_BASE* with the directory where the METplotpy source code + is saved, e.g.: + + /home/username/working/METplotpy + + +Configuration Files +------------------- + +The mode field plot utilizes YAML configuration files to indicate where input +data is located and to set plot attributes. YAML is a recursive +acronym for "YAML Ain't Markup Language" and according to +`yaml.org `_, it is a +"human-friendly data serialization language". +It is commonly used for configuration files and in applications where +data is being stored or +transmitted. Two configuration files are required. The first is a default +configuration file, **mode_field_plot_defaults.yaml**, which is found in the +*$METPLOTPY_BASE/metplotpy/plots/config* directory. All default +configuration files are located in the +*$METPLOTPY_BASE/metplotpy/plots/config* directory. +*$METPLOTPY_BASE* is base directory where the +METplotpy source code has been saved. **Default configuration files are +automatically loaded by the plotting code and do not need to be explicitly +specified when generating a plot**. In addition, the default configuration file +**DOES NOT require any modifications**. + +The second required configuration file is a user-supplied “custom” +configuration file. This file is used to customize/override the default +settings in the **mode_field_plot_defaults.yaml** file. The custom configuration +file can contain only those settings that will override the default settings in the +mode_field_plot_defaults.yaml config file. + +.. note:: + + The YAML configuration files accept environment variables. To specify + a setting using an environment variable, use the following syntax: + + setting_name: !ENV '${YOUR_ENV}' + + e.g. + + .. code-block:: + + file_dir: !ENV '${WORKING_DIR}' + + the example above uses the environment variable *WORKING_DIR* to specify the *file_dir* setting + + + e.g. + + .. code-block:: + + file_dir: !ENV '${WORKING_DIR}/experiment' + + the example above sets the *file_dir* setting to the *experiment* subdirectory of the directory defined by the *WORKING_DIR* environment variable + +METplus Configuration +===================== + +Default Configuration File +-------------------------- + +The *mandatory*, **mode_field_plot_defaults.yaml** configuration file serves as a good starting point for creating a mode field +plot. This default config file **SHOULD NOT** be modified. +The custom configuration file is used to override the settings +of interest (i.e. marker colors, marker styles, trendline styles, etc.). + + +.. note:: + + This default configuration file is automatically loaded by **mode_field_plot.py**. + +**Default configuration file:** + +.. dropdown:: mode_field_plot_defaults.yaml (click to expand) + + .. literalinclude:: ../../metplotpy/plots/config/mode_field_plot_defaults.yaml + + +**DO NOT modify the default configuration file.** + + +Custom Configuration File +------------------------- + +A second, *mandatory* configuration file is required, which is +used to customize the settings to the mode field plot. The **test_mode_field_plot_objects.yaml** and **test_mode_field_plot_raw.yaml** configuration files are included with the source code. + + +.. dropdown:: **Example config to plot MODE objects fields** (click to expand) + + .. literalinclude:: ../../test/mode_field_plot/test_mode_field_plot_objects.yaml + +.. dropdown:: **Example config to plot MODE raw field** (click to expand) + + .. literalinclude:: ../../test/mode_field_plot/test_mode_field_plot_raw.yaml + +To generate the plot of the MODE object field, copy the custom config file from the directory where the source code was saved to the working directory: + +.. code-block:: ini + + cp $METPLOTPY_BASE/test/mode_field_plot/test_mode_field_plot_objects.yaml $WORKING_DIR/custom_mode_field_obj_plot.yaml + +*$METPLOTPY_BASE* is the full path to the METplotpy source code, e.g. /users/someuser/working/METplotpy + +*$WORKING_DIR* is the directory where the custom config files will be saved and modified + +To generate a plot of the MODE raw field, copy this custom config file to the working directory: + +.. code-block:: ini + + cp $METPLOTPY_BASE/test/mode_field_plot/test_mode_field_plot_raw.yaml $WORKING_DIR/custom_mode_field_raw_plot.yaml + +*$METPLOTPY_BASE* is the full path to the METplotpy source code, e.g. + /home/username/working_directory/METplotpy + +*$WORKING_DIR* is the directory where the custom config files will be saved and modified + +**Make modifications to the custom configuration file** + + .. Note:: To customize the plots, replace the existing value with the desired value. To use the default setting, comment out or delete the setting. + +.. dropdown:: Description of custom config file settings (click to expand) + + .. dropdown:: mode_obj_file + + - **mandatory** + + - MODE netCDF file + + - full path to the file, replace *path-to-data* with full path + + ex: $METplotpy_BASE/test/mode_field_plot/mode_KWBC_APCP_24_vs_IMERG_APCP_24_960000L_20240207_000000V_240000A_R1_T2_obj.nc + + where *$METPLOTPY_BASE* is the path to the METplotpy source directory + + .. dropdown:: output_dir + + - **mandatory** + + .. dropdown:: output_filename + + - **mandatory** + + .. dropdown:: field_to_plot + + - options are *objects* or *raw* + + - default is *objects* + + + .. dropdown:: super_title + + - **mandatory** + + - the super title (at the very top of the plot figure) + + - default is "Your super-title goes here" + + .. dropdown:: super_title_font_size + + - default is 12 + + + .. dropdown:: download_natural_earth_shapefile + + - default value is False + + - uses the provided Natural Earth shapefile (included in source code) + + + - if set to True, downloads from the naturalearthdata.com/S3 mirror + + - **NOTE** requires internet access + + .. dropdown:: plot_height + + - value in inches + + - default value set to 8 + + + .. dropdown:: plot_width + + - value in inches + + - default value set to 11.0 + + .. dropdown:: bbox_padding_degrees + + - padding around the bounding box + + - default value is 5.0 + + + .. dropdown:: resolution_dpi + + - resolution in dpi (dots per inch) + + - default value is 200 + + .. dropdown:: show_obj_id_labels + + - shows object id labels + + - default value is True + + + .. dropdown:: vert_spacing + + - vertical spacing between panels + + - default value is 0.06 + + .. dropdown:: colormap_name + + - colormap used for plotting MODE raw field + + - default value is *turbo* + + - for a list of colormaps supported in Matplotlib, please refer to: ``_ + + + .. dropdown:: colorbar_max + + - max value for colorbar in legend for MODE field + + - default value is 99.0 + + .. dropdown:: colorbar_label + + - label to colorbar + + - default is 'colorbar label goes here' + + .. dropdown:: colorbar_label_fontsize + + - fontsize for the colorbar legend label + + - default value is 9 + + .. dropdown:: title_space + + - title spacing between panels + + - default value is 0.45 + + .. dropdown:: top_bottom_marging + + - positioning the super title + + - default value is 0.35 + + Applicable to plotting the raw field data: range + that the colormap covers + + .. dropdown:: vmin + + - default value is 0.0 + + .. dropdown:: vmax + + - default is None (i.e. not specified/setting not + included in the config file) + + .. dropdown:: vmax_pctile + + - used to comput vmax when vmax is unspecified + + - default value is 99.0 + + + +Run from the Command Line +========================= + +**MODE field objects plot** + +The **custom_mode_field_obj_plot.yaml** configuration file, in combination with the +**mode_field_plot_defaults.yaml** configuration file, generates a figure of the MODE objects field. + +The MODE objects figure has two stacked plots, the upper plot is the forecast objects +and the lower plot is the observation objects: + +.. image:: figure/apcp_example_mode_objects.png + +To generate the above plot using the **mode_field_plot_defaults.yaml** and +**custom_mode_field_obj_plot.yaml** config files, perform the following: + +* If using the conda environment, verify the conda environment + is running and has the required + `Python packages `_ + outlined in the requirements section. + +* Set the METPLOTPY_BASE environment variable to point to + *$METPLOTPY_BASE*. + + For the ksh environment: + + .. code-block:: ini + + export METPLOTPY_BASE=$METPLOTPY_BASE + + export WORKING_DIR=$WORKING_DIR + + For the csh environment: + + .. code-block:: ini + + setenv METPLOTPY_BASE $METPLOTPY_BASE + + setenv WORKING_DIR $WORKING_DIR + + Recall that *$METPLOTPY_BASE* is the directory path indicating where the METplotpy source code was saved. + + *$WORKING_DIR* is the directory path where config files are to be saved and modified + +* Enter the following command: + + .. code-block:: ini + + python $METPLOTPY_BASE/metplotpy/plots/mode_field_plot/mode_field_plot.py $WORKING_DIR/custom_mode_field_obj_plot.yaml + + +* An **apcp_example_mode_objects.png** MODE objects plot file will be created in the directory specified in the *plot_filename* configuration setting in the **custom_mode_field_obj_plot.yaml** config file. + + + +**MODE field raw plot** + +The **custom_mode_field_raw_plot.yaml** configuration file, in combination with the +**mode_field_plot_defaults.yaml** configuration file, generates a figure of the MODE raw field. + +The MODE raw figure has two stacked plots for the raw field. + +.. image:: figure/apcp_example_mode_raw.png + +To generate the above plot using the **mode_field_plot_defaults.yaml** and +**custom_mode_field_raw_plot.yaml** config files, perform the following: + +* If using the conda environment, verify the conda environment + is running and has the required + `Python packages `_ + outlined in the requirements section. + +* Set the METPLOTPY_BASE environment variable to point to + *$METPLOTPY_BASE*. + + For the ksh environment: + + .. code-block:: ini + + export METPLOTPY_BASE=$METPLOTPY_BASE + + export WORKING_DIR=$WORKING_DIR + + For the csh environment: + + .. code-block:: ini + + setenv METPLOTPY_BASE $METPLOTPY_BASE + + setenv WORKING_DIR $WORKING_DIR + + Recall that *$METPLOTPY_BASE* is the directory path indicating where the METplotpy source code was saved. + + *$WORKING_DIR* is the directory path where config files are to be saved and modified + + +* Enter the following command: + + .. code-block:: ini + + python $METPLOTPY_BASE/metplotpy/plots/mode_field_plot/mode_field_plot.py $WORKING_DIR/custom_mode_field_raw_plot.yaml + + +* An **apcp_example_mode_raw.png** MODE objects plot file will be created in the directory specified in the *plot_filename* configuration setting in the **custom_mode_field_raw_plot.yaml** config file. + + diff --git a/metplotpy/plots/config/mode_field_plot_defaults.yaml b/metplotpy/plots/config/mode_field_plot_defaults.yaml new file mode 100644 index 000000000..74488229e --- /dev/null +++ b/metplotpy/plots/config/mode_field_plot_defaults.yaml @@ -0,0 +1,80 @@ +# Configuration file for generating the mode field plot + +# Reads MODE netCDF files and generates a plot for each file in the directory +input_dir: /path/to/mode_ncdata + +# plot specific file +mode_obj_file: ' ' + +output_dir: ' ' +output_filename: ' ' + +# Logging level: DEBUG, INFO, WARN, ERROR +log_level: WARN +log_filename: 'stdout' + +# Field to plot: objects or raw +field_to_plot: objects + +# Super title (title at the very top) +super_title: 'Your super-title goes here' +super_title_font_size: 12 + +# Download via Natural Earth downloader +# *Note* Requires internet access to the +# naturalearthdata.com/S3 mirror +# If False, use the provided Natural Earth shapefile +download_natural_earth_shapefile: False + +# Plot settings +plot_height: 8 +plot_width: 11.0 + +# padding around the bbox, +# omit/comment this setting to use default value of 5.0 degrees +bbox_padding_degrees: 5.0 + +# resolution in dpi, omit/comment this setting to use the default value of 200 +#dots-per-inch +resolution_dpi: 200 + +# object id labels, omit/comment this setting to use default value of True +show_obj_id_labels: True + +# vertical spacing between panels, omit/comment this setting to use +# default value of 0.06 +vert_spacing: 0.06 + +# colormap, omit/comment this setting to use default value of "turbo" +colormap_name: turbo + +# colorbar max for field, omit/comment this setting to use default of 99th +# percentile +colorbar_max: 99.0 + +colorbar_label: 'colorbar label goes here' +colorbar_label_fontsize: 9 + +# for plotting raw field +# data range that the colormap covers +vmin: 0.0 +# vmax is None by default (i.e. not specified) + +# used to compute vmax if it vmax is not specified +vmax_pctile: 99.0 + +# title spacing between panels +title_space: 0.45 + +# for positioning the super title +top_bottom_margin: 0.35 + + + + + + + + + + diff --git a/metplotpy/plots/mode_field_plot/__init__.py b/metplotpy/plots/mode_field_plot/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/metplotpy/plots/mode_field_plot/mode_field_plot.py b/metplotpy/plots/mode_field_plot/mode_field_plot.py new file mode 100644 index 000000000..4b7c1e337 --- /dev/null +++ b/metplotpy/plots/mode_field_plot/mode_field_plot.py @@ -0,0 +1,454 @@ +# ============================* +# ** Copyright UCAR (c) 2026 +# ** University Corporation for Atmospheric Research (UCAR) +# ** National Science Foundation National Center for Atmospheric Research (NSF NCAR) +# ** Research Applications Lab (RAL) +# ** P.O.Box 3000, Boulder, Colorado, 80307-3000, USA +# ============================* + +""" +Class Name: ModeFieldPlot + """ +__author__ = 'Michelle Harrold, Minna Win' + +import os +import sys +from datetime import datetime +import yaml + +import numpy as np +import netCDF4 as nc +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import matplotlib.patheffects # noqa: F401 (used inside add_object_labels) +from matplotlib.colors import ListedColormap, BoundaryNorm +import cartopy.crs as ccrs +import cartopy.io.shapereader as shpreader +from cartopy.feature import ShapelyFeature +from metplotpy.plots.mode_field_plot.mode_field_plot_config import ModeFieldPlotConfig +from metplotpy.plots import util +from metplotpy.plots.util import get_common_logger as logging + + +class ModeFieldPlot: + """ + Python implementation of the deprecated MET application: plot_mode_field + This code is a configurable version of the script created by Michelle Harrold. + + A default configuration file in the METplotpy/metplotpy/plots/config directory + contains default values for plot settings. The user provides a *required* + configuration file to customize the plot based on their data. + + """ + + + def __init__(self, params: dict) -> None: + default_conf_filename = "mode_field_plot_defaults.yaml" + + # Determine location of the default YAML config files and then + # read defaults stored in YAML formatted file into the dictionary + if 'METPLOTPY_BASE' in os.environ: + location = os.path.join(os.environ['METPLOTPY_BASE'], 'metplotpy/plots/config') + else: + location = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'config')) + + defaults:dict = util.parse_config(os.path.join(location, default_conf_filename)) + + # merge user defined parameters into defaults + if params: + self.settings = {**defaults, **params} + else: + self.settings = defaults + + # instantiate the config object + self.config_obj = ModeFieldPlotConfig(self.settings) + self.logger = self.config_obj.logger + + # Make the output directory in the event it doesn't already exist + os.makedirs(self.settings['output_dir'], exist_ok=True) + + # Retrieve the Natural Earth shapefile either + # online, or use the shapefile in the mode_field_plot + # directory + if self.config_obj.download_shapefile: + shp_path = shpreader.natural_earth( + resolution="50m", category="physical", name="coastline" + ) + reader = shpreader.Reader(shp_path) + else: + # use existing shapefile + srcdir = os.path.dirname(__file__) + parent = os.path.join(srcdir, "shapefile") + local_shapefile = os.path.join(parent, "ne_50m_coastline.shp") + reader = shpreader.Reader(local_shapefile) + + geoms = list(reader.geometries()) + self.shapefile_geoms = geoms + + + def plot_mode_objects(self) -> None: + """ + Plot the raw forecast and observation fields (same layout/crop as + plot_mode_objects), sharing one colorbar for direct visual comparison. + + Args: + :param self: used to retrieve the necessary plot settings defined in the + YAML config file + :return: None + + """ + + nc_path = self.config_obj.input_file + out_path = self.config_obj.output_filename + pad = self.config_obj.padding + dpi = int(self.config_obj.resolution_dpi) + label_objects: bool = self.config_obj.labels_on + fig_width = self.config_obj.plot_width + hspace = self.config_obj.vert_spacing + super_title_text = self.config_obj.super_title_text + super_title_fontsize = self.config_obj.super_title_font_size + + subplot_top = self.config_obj.subplot_adjust_top + subplot_bottom = self.config_obj.subplot_adjust_bottom + subplot_right = self.config_obj.subplot_adjust_right + subplot_left = self.config_obj.subplot_adjust_left + + d = self.load_mode_obj(nc_path) + + lon, (fcst_id, fcst_clus, obs_id, obs_clus, fcst_raw, obs_raw) = self.roll_to_pm180( + d["lon"], d["fcst_obj_id"], d["fcst_clus_id"], d["obs_obj_id"], + d["obs_clus_id"], d["fcst_raw"], d["obs_raw"], + ) + + lat = d["lat"] + + extent = self.get_extent(lat, lon, fcst_id, obs_id, pad=pad) + + ids, color_map = self.build_cluster_cmap([fcst_clus, obs_clus]) + fcst_rgba = self.build_rgba(fcst_id, fcst_clus, color_map) + obs_rgba = self.build_rgba(obs_id, obs_clus, color_map) + + coast_geoms = self.shapefile_geoms + proj = ccrs.PlateCarree() + + # Cartopy's PlateCarree axes enforce a 1:1 data aspect ratio, so we size + # the figure to match the extent's aspect ratio ourselves -- otherwise + # matplotlib pads out the leftover space as big gaps around each map. + lon_range = extent[1] - extent[0] + lat_range = extent[3] - extent[2] + map_aspect = lon_range / lat_range # width / height, per panel + map_height = fig_width / map_aspect + title_space = self.config_obj.title_space # per-panel title + top_bottom_margin = self.config_obj.top_bottom_margin # suptitle allowance + fig_height = 2 * (map_height + title_space) + top_bottom_margin + + fig, axes = plt.subplots( + 2, 1, figsize=(fig_width, fig_height), subplot_kw={"projection": proj}, + ) + + fig.subplots_adjust(hspace=hspace, top=subplot_top, bottom=subplot_bottom, left=subplot_left, + right=subplot_right) + fcst_init_str = util.parse_met_time(d["fcst_init_time"]) + fcst_valid_str = util.parse_met_time(d["fcst_valid_time"]) + obs_valid_str = util.parse_met_time(d["obs_valid_time"]) + + fcst_time_line = "" + if fcst_init_str or fcst_valid_str: + parts = [] + if fcst_init_str: + parts.append(f"Init: {fcst_init_str}") + if fcst_valid_str: + parts.append(f"Valid: {fcst_valid_str}") + fcst_time_line = " | " + " ".join(parts) + + obs_time_line = f" | Valid: {obs_valid_str}" if obs_valid_str else "" + + titles = [ + f"Forecast objects | {d['model']} {d['fcst_var']} {d['fcst_level']} " + f"({d['fcst_thresh']}){fcst_time_line}", + f"Observation objects | {d['obtype']} {d['obs_var']} {d['obs_level']} " + f"({d['obs_thresh']}){obs_time_line}", + ] + rgba_fields = [fcst_rgba, obs_rgba] + + for ax, rgba, title in zip(axes, rgba_fields, titles): + ax.set_extent(extent, crs=proj) + coast_feat = ShapelyFeature( + coast_geoms, proj, facecolor="0.92", edgecolor="0.35", linewidth=0.5 + ) + ax.add_feature(coast_feat, zorder=0) + ax.imshow( + rgba, origin="lower", extent=[lon.min(), lon.max(), lat.min(), lat.max()], + transform=proj, interpolation="nearest", zorder=2, + ) + gl = ax.gridlines(draw_labels=True, linewidth=0.3, color="gray", alpha=0.5) + gl.top_labels = False + gl.right_labels = False + ax.set_title(title, fontsize=11, fontweight="bold") + + if label_objects: + self.add_object_labels(axes[0], fcst_id, lat, lon, proj) + self.add_object_labels(axes[1], obs_id, lat, lon, proj) + + fig.suptitle( + super_title_text, + fontsize=super_title_fontsize, + ) + + fig.savefig(out_path, dpi=dpi, bbox_inches="tight") + plt.tight_layout() + self.logger.info(f"Wrote {out_path}") + + + def plot_mode_raw(self): + """Plot the raw forecast and observation fields (same layout/crop as + plot_mode_objects), sharing one colorbar for direct visual comparison.""" + + nc_path = self.config_obj.input_file + out_path = self.config_obj.output_filename + pad = self.config_obj.padding + dpi = int(self.config_obj.resolution_dpi) + label_objects: bool = self.config_obj.labels_on + fig_width = self.config_obj.plot_width + hspace = self.config_obj.vert_spacing + super_title_text = self.config_obj.super_title_text + super_title_fontsize = self.config_obj.super_title_font_size + cmap_name = self.config_obj.cmap + vmin = self.config_obj.vmin + vmax = self.config_obj.vmax + + d = self.load_mode_obj(nc_path) + + lon, (fcst_id, obs_id, fcst_raw, obs_raw) = self.roll_to_pm180( + d["lon"], d["fcst_obj_id"], d["obs_obj_id"], d["fcst_raw"], d["obs_raw"], + ) + lat = d["lat"] + + # Reuse the same object-derived bounding box so the raw-field plot lines + # up with the object plot at the same zoom level. + extent = self.get_extent(lat, lon, fcst_id, obs_id, pad=pad) + + fcst_plot = np.ma.masked_less(np.ma.masked_invalid(fcst_raw), 0) + obs_plot = np.ma.masked_less(np.ma.masked_invalid(obs_raw), 0) + + if vmax is None: + combined = np.concatenate([fcst_plot.compressed(), obs_plot.compressed()]) + vmax = float(np.percentile(combined, self.config_obj.vmax_pctile)) if combined.size else 1.0 + + coast_geoms = self.shapefile_geoms + proj = ccrs.PlateCarree() + + lon_range = extent[1] - extent[0] + lat_range = extent[3] - extent[2] + map_aspect = lon_range / lat_range + map_height = fig_width / map_aspect + title_space = self.config_obj.title_space + top_bottom_margin = self.config_obj.top_bottom_margin + fig_height = 2 * (map_height + title_space) + top_bottom_margin + + fig, axes = plt.subplots( + 2, 1, figsize=(fig_width, fig_height), subplot_kw={"projection": proj}, + ) + # Leave extra room on the right for a shared colorbar. + fig.subplots_adjust(hspace=hspace, top=0.90, bottom=0.04, left=0.05, right=0.88) + + fcst_init_str = util.parse_met_time(d["fcst_init_time"]) + fcst_valid_str = util.parse_met_time(d["fcst_valid_time"]) + obs_valid_str = util.parse_met_time(d["obs_valid_time"]) + + fcst_time_line = "" + if fcst_init_str or fcst_valid_str: + parts = [] + if fcst_init_str: + parts.append(f"Init: {fcst_init_str}") + if fcst_valid_str: + parts.append(f"Valid: {fcst_valid_str}") + fcst_time_line = " | " + " ".join(parts) + obs_time_line = f" | Valid: {obs_valid_str}" if obs_valid_str else "" + + titles = [ + f"{d['model']} {d['fcst_var']} {d['fcst_level']} " + # f"({d['fcst_units']}){fcst_time_line}", + f"(mm){fcst_time_line}", + f"{d['obtype']} {d['obs_var']} {d['obs_level']} " + # f"({d['obs_units']}){obs_time_line}", + f"(mm)", + ] + fields = [fcst_plot, obs_plot] + + im = None + for ax, field, title in zip(axes, fields, titles): + ax.set_extent(extent, crs=proj) + im = ax.imshow( + field, origin="lower", extent=[lon.min(), lon.max(), lat.min(), lat.max()], + transform=proj, cmap=cmap_name, vmin=vmin, vmax=vmax, + interpolation="nearest", zorder=2, + ) + # Coastlines drawn as outline-only on top of the data (no fill), + # since the data covers the whole domain here. + coast_feat = ShapelyFeature( + coast_geoms, proj, facecolor="none", edgecolor="black", linewidth=0.6 + ) + ax.add_feature(coast_feat, zorder=3) + gl = ax.gridlines(draw_labels=True, linewidth=0.3, color="gray", alpha=0.5) + gl.top_labels = False + gl.right_labels = False + ax.set_title(title, fontsize=11, fontweight="bold") + + cbar = fig.colorbar(im, ax=axes, shrink=0.75, pad=0.02, extend="max") + if len(self.config_obj.colorbar_label) == 0: + cbar.set_label(f"{d['fcst_var']} ({d['fcst_units']})", fontsize=self.config_obj.colorbar_label_fontsize) + else: + cbar.set_label(self.config_obj.colorbar_label, fontsize=self.config_obj.colorbar_label_fontsize) + + fig.suptitle(super_title_text, + fontsize=super_title_fontsize + ) + + fig.savefig(out_path, dpi=dpi, bbox_inches="tight") + print(f"Wrote {out_path}") + + + def build_cluster_cmap(self, clus_ids_list): + """Build a consistent discrete colormap for cluster ids across panels.""" + all_ids = set() + for arr in clus_ids_list: + vals = arr.compressed() if np.ma.is_masked(arr) else arr.ravel() + vals = vals[vals > 0] + all_ids.update(np.unique(vals).tolist()) + ids = sorted(all_ids) + + base_colors = plt.get_cmap("tab20").colors + plt.get_cmap("tab20b").colors + color_map = {cid: base_colors[i % len(base_colors)] for i, cid in enumerate(ids)} + return ids, color_map + + + def add_object_labels(self, ax, obj_id, lat, lon, proj, fontsize=7): + """Label each individual object with its object id at its centroid.""" + data = obj_id.filled(-1) if np.ma.is_masked(obj_id) else obj_id + ids = np.unique(data) + ids = ids[ids > 0] + lon2d, lat2d = np.meshgrid(lon, lat) + for oid in ids: + m = data == oid + clat = lat2d[m].mean() + clon = lon2d[m].mean() + ax.text( + clon, clat, str(int(oid)), + transform=proj, fontsize=fontsize, fontweight="bold", + ha="center", va="center", color="black", + path_effects=[ + matplotlib.patheffects.withStroke(linewidth=2, foreground="white"), + ], + ) + + + def load_mode_obj(self, path): + ds = nc.Dataset(path) + d = { + "lat": ds.variables["lat"][:], + "lon": ds.variables["lon"][:], + "fcst_raw": ds.variables["fcst_raw"][:], + "fcst_obj_id": ds.variables["fcst_obj_id"][:], + "fcst_clus_id": ds.variables["fcst_clus_id"][:], + "obs_raw": ds.variables["obs_raw"][:], + "obs_obj_id": ds.variables["obs_obj_id"][:], + "obs_clus_id": ds.variables["obs_clus_id"][:], + "fcst_var": util.decode_char_var(ds.variables["fcst_variable"]), + "obs_var": util.decode_char_var(ds.variables["obs_variable"]), + "fcst_level": util.decode_char_var(ds.variables["fcst_level"]), + "obs_level": util.decode_char_var(ds.variables["obs_level"]), + "fcst_thresh": util.decode_char_var(ds.variables["fcst_conv_threshold"]), + "obs_thresh": util.decode_char_var(ds.variables["obs_conv_threshold"]), + "fcst_units": util.decode_char_var(ds.variables["fcst_units"]), + "obs_units": util.decode_char_var(ds.variables["obs_units"]), + "model": getattr(ds, "model", ""), + "obtype": getattr(ds, "obtype", ""), + "fcst_init_time": getattr(ds.variables["fcst_raw"], "init_time", None), + "fcst_valid_time": getattr(ds.variables["fcst_raw"], "valid_time", None), + "obs_valid_time": getattr(ds.variables["obs_raw"], "valid_time", None), + } + ds.close() + return d + + + def build_rgba(self, obj_id, clus_id, color_map, unmatched_color=(0.0, 0.0, 1.0)): + """Build an RGBA image from object/cluster ids. + + Any pixel that belongs to an object (obj_id valid) is colored: + - by its cluster's color, if that cluster matched across fcst/obs + (clus_id > 0) + - by `unmatched_color` (default blue), if the object had no match + (clus_id == -1, MODE's code for "no cluster/pair") + """ + obj_data = obj_id.filled(-1) if np.ma.is_masked(obj_id) else obj_id + clus_data = clus_id.filled(-1) if np.ma.is_masked(clus_id) else clus_id + + h, w = obj_data.shape + rgba = np.zeros((h, w, 4), dtype=float) + + has_obj = obj_data > 0 + matched = has_obj & (clus_data > 0) + unmatched = has_obj & (clus_data <= 0) + + for cid, color in color_map.items(): + m = matched & (clus_data == cid) + if not m.any(): + continue + rgba[m, 0] = color[0] + rgba[m, 1] = color[1] + rgba[m, 2] = color[2] + rgba[m, 3] = 0.75 + + if unmatched.any(): + rgba[unmatched, 0] = unmatched_color[0] + rgba[unmatched, 1] = unmatched_color[1] + rgba[unmatched, 2] = unmatched_color[2] + rgba[unmatched, 3] = 0.75 + + return rgba + + + def roll_to_pm180(self, lon, *fields): + """Roll a 0-360 lon axis (and matching 2D fields) to -180..180.""" + lon = np.asarray(lon) + lon_adj = np.where(lon > 180, lon - 360, lon) + order = np.argsort(lon_adj) + lon_out = lon_adj[order] + fields_out = [f[:, order] for f in fields] + return lon_out, fields_out + + + def get_extent(self, lat, lon, *id_fields, pad=5.0): + """Bounding box (lon-lat) covering all non-missing object pixels, padded.""" + mask = np.zeros(id_fields[0].shape, dtype=bool) + for f in id_fields: + mask |= ~np.ma.getmaskarray(f) + rows = np.where(mask.any(axis=1))[0] + cols = np.where(mask.any(axis=0))[0] + lat_min, lat_max = lat[rows.min()], lat[rows.max()] + lon_min, lon_max = lon[cols.min()], lon[cols.max()] + return [ + max(lon_min - pad, -180), + min(lon_max + pad, 180), + max(lat_min - pad, -90), + min(lat_max + pad, 90), + ] + + +def main(config_filename=None): + # Read in the YAML configuration file. Environment variables in + # the configuration file are supported. + settings:dict = util.get_params(config_filename) + + mfp = ModeFieldPlot(settings) + if mfp.config_obj.field_to_plot == "raw": + mfp.plot_mode_raw() + else: + mfp.plot_mode_objects() + + +if __name__ == "__main__": + main() diff --git a/metplotpy/plots/mode_field_plot/mode_field_plot.yaml b/metplotpy/plots/mode_field_plot/mode_field_plot.yaml new file mode 100644 index 000000000..ab0959047 --- /dev/null +++ b/metplotpy/plots/mode_field_plot/mode_field_plot.yaml @@ -0,0 +1,71 @@ +# Configuration file for generating the mode field plot + +# Reads MODE netCDF files and generates a plot for each file in the directory +input_path: /path/to/mode_ncdata + +# plot specific file(s) +input_files: [] + +output_dir: '' + +output_filename: + +log_dir: + +# Logging level: DEBUG, INFO, WARN, ERROR +log_level: DEBUG + +# Field to plot: objects or raw +field: objects + +# use full extent of data +use_full_extent: True + +#Map extent, used only if use_full_extent is False +north_lat: '' +south_lat: '' +east_lon: '' +west_lon: '' + +# Title, if empty, create one based on data and plotting criteria +title: '' + +# Sub title +sub_title: '' + +# Fields of interest +fields: [] + +# Plot settings + +# padding around the bbox, +# omit/comment this setting to use default value of 5.0 degrees +padding_degrees: 5.0 + +# resolution in dpi, omit/comment this setting to use the default value of 200 +#dots-per-inch +resolution_dpi: 200 + +# object id labels, omit/comment this setting to use default value of True +show_obj_id_labels: True + +# vertical spacing between panels, omit/comment this setting to use +# default value of 0.06 +vert_spacing: 0.06 + +# colormap, omit/comment this setting to use default value of "turbo" +colormap: turbo + +# colorbar max for field, omit/comment this setting to use default of 99th +# percentile +colorbar_max: 99.0 + + + + + + + + + + diff --git a/metplotpy/plots/mode_field_plot/mode_field_plot_config.py b/metplotpy/plots/mode_field_plot/mode_field_plot_config.py new file mode 100644 index 000000000..99a091d23 --- /dev/null +++ b/metplotpy/plots/mode_field_plot/mode_field_plot_config.py @@ -0,0 +1,109 @@ +# ============================* +# ** Copyright UCAR (c) 2026 +# ** University Corporation for Atmospheric Research (UCAR) +# ** National Science Foundation National Center for Atmospheric Research (NSF NCAR) +# ** Research Applications Lab (RAL) +# ** P.O.Box 3000, Boulder, Colorado, 80307-3000, USA +# ============================* + +""" +Class Name: ModeFieldPlotConfig + +Handles configuration settings for plotting the Mode fields + +Default configuration file settings are overridden by a mandatory, user-specified +configuration file. + +""" +__author__ = 'Minna Win' + +import os +from .. import util + + +class ModeFieldPlotConfig(): + + def __init__(self, parameters: dict): + + self.params = parameters + + log_level: str = self.get_config_value('log_level') + log_filename = self.get_config_value('log_filename') + self.logger = util.get_common_logger(log_level, log_filename) + + input_dir = self.get_config_value('input_dir') + input_data = self.get_config_value('mode_obj_file') + self.input_file = os.path.join(input_dir, input_data) + self.field_to_plot: str = self.get_config_value('field_to_plot') + + output_dir = self.get_config_value('output_dir') + output_fname = self.get_config_value('output_filename') + self.output_filename = os.path.join(output_dir, output_fname) + + self.download_shapefile: bool = self.get_config_value('download_natural_earth_shapefile') + + if self.get_config_value('plot_filename') is None or self.get_config_value('plot_filename') == ' ': + if self.field_to_plot.lower() == 'raw': + plot_name = "mode_raw.png" + else: + # name the objects field plot + self.plot_filename = "mode_objects.png" + else: + # user-defined plot filename + self.plot_filename = self.get_config_value('plot_filename') + + # Plot settings + self.plot_width = self.get_config_value('plot_width') + self.plot_height = self.get_config_value('plot_height') + self.padding: float = self.get_config_value('bbox_padding_degrees') + self.resolution_dpi: float = self.get_config_value('resolution_dpi') + self.auto_space: bool = self.get_config_value('automatic_vert_horiz_spacing') + self.labels_on: bool = self.get_config_value('show_obj_id_labels') + # colormap settings + self.cmap: str = self.get_config_value('colormap_name') + self.vmin: float = self.get_config_value('vmin') + self.vmax: float = self.get_config_value('vmax') + self.vmax_pctile: float = self.get_config_value('vmax_pctile') + + self.colorbar_label: str = self.get_config_value('colorbar_label') + self.colorbar_label_fontsize: int = self.get_config_value('colorbar_label_fontsize') + self.colorbar_max: float = self.get_config_value('colorbar_max') + + self.title_space: float = self.get_config_value('title_space') + self.top_bottom_margin: float = self.get_config_value('top_bottom_margin') + self.super_title_text = self.get_config_value('super_title') + self.super_title_font_size = self.get_config_value('super_title_font_size') + + self.subplot_adjust_top = self.get_config_value('subplot_top') + self.subplot_adjust_bottom = self.get_config_value('subplot_bottom') + self.subplot_adjust_left = self.get_config_value('subplot_left') + self.subplot_adjust_right = self.get_config_value('subplot_right') + self.vert_spacing = self.get_config_value('vert_spacing') + + + def get_config_value(self, *args: str | int | float): + """ + Gets the value of a configuration parameter. + Looks for parameter in the user's parameter dictionary + + Args: + @ param args - chain of keys that defines a key to the parameter + + Returns: + - a value for the parameter of None + """ + + if args and self.params: + + # get value for the first key + element = args[0] + if element: + value = self.params.get(element) + + # if the size of key tuple is 1 - the search is over + if len(args) == 1: + return value + + # if the size of key tuple is > 1 - search using other keys + return self._get_nested(value, args[1:]) + return None diff --git a/metplotpy/plots/mode_field_plot/mode_field_plot_objects.yaml b/metplotpy/plots/mode_field_plot/mode_field_plot_objects.yaml new file mode 100644 index 000000000..4909d2f1d --- /dev/null +++ b/metplotpy/plots/mode_field_plot/mode_field_plot_objects.yaml @@ -0,0 +1,63 @@ +# Configuration file for generating the mode field plot + +# +# Comment setting values if using default values set in the default config file +# + +# Reads MODE netCDF files and generates a plot for the specified file +mode_obj_file: 'your-full-path-to/sample-data/mode_KWBC_APCP_24_vs_IMERG_APCP_24_960000L_20240207_000000V_240000A_R1_T2_obj.nc' + +output_dir: 'your-output-dir' +output_filename: 'dev_mode_objects.png' + +# Logging level: DEBUG, INFO, WARN, ERROR +log_level: DEBUG + +# Log file, include full path +log_filename: 'your-path-to-log-dir/objects_log.txt' + +# Field to plot: objects or raw +field_to_plot: objects + +# Super title +super_title: "MODE Object Field: Matched Forecast vs. Observation" +super_title_font_size: 12 + + +# Download via Natural Earth downloader +# *Note* Requires internet access to the +# naturalearthdata.com/S3 mirror +# If False, use the provided Natural Earth shapefile +download_natural_earth_shapefile: False + +# +# Plotting specific settings +# To specify custom values, uncomment setting name +# and add your value +#plot_width: 11 +#plot_height: 8 + +# padding around the bbox, +#bbox_padding_degrees: 5.0 + +# resolution in dpi +#resolution_dpi: 200 + +# turn on/off displaying object id labels +show_obj_id_labels: True + +# vertical spacing between panels +#vert_spacing: 0.06 + +# colormap +colormap_name: turbo + +# colorbar max for field, omit/comment this setting to use default of 99th +# percentile +#colorbar_max: 99.0 + +colorbar_label: "Accum precip (mm)" + + + + diff --git a/metplotpy/plots/mode_field_plot/mode_field_plot_raw.yaml b/metplotpy/plots/mode_field_plot/mode_field_plot_raw.yaml new file mode 100644 index 000000000..d6da82f51 --- /dev/null +++ b/metplotpy/plots/mode_field_plot/mode_field_plot_raw.yaml @@ -0,0 +1,68 @@ +# Configuration file for generating the mode field plot + +# +# Comment setting values if using default values set in the default config file +# + +# Reads MODE netCDF files and generates a plot for the specified file +mode_obj_file: 'your-full-path-to/sample-data/mode_KWBC_APCP_24_vs_IMERG_APCP_24_960000L_20240207_000000V_240000A_R1_T2_obj.nc' + +output_dir: 'your-path-to-output' +output_filename: 'dev_mode_raw.png' + +# Logging level: DEBUG, INFO, WARN, ERROR +log_level: DEBUG + +# Log file, include full path +log_filename: 'your-log-dir/raw_log.txt' + +# Field to plot: objects or raw +field_to_plot: raw + +# Super title +super_title: "MODE Raw Fields: Matched Forecast vs. Observation" +super_title_font_size: 12 + + +# Download via Natural Earth downloader +# *Note* Requires internet access to the +# naturalearthdata.com/S3 mirror +# If False, use the provided Natural Earth shapefile +download_natural_earth_shapefile: False + +# +# Plotting specific settings +# To specify custom values, uncomment setting name +# and add your value, otherwise leave commented to +# use default values in the mode_field_plot_defaults.yaml +# config file under metplotpy/plots/mode_field_plot/config +# + +#plot_width: 11 +#plot_height: 8 + +# padding around the bbox, +#bbox_padding_degrees: 5.0 + +# resolution in dpi +#resolution_dpi: 200 + +# turn on/off displaying object id labels +show_obj_id_labels: True + +# vertical spacing between panels +#vert_spacing: 0.06 + +# colormap +colormap_name: rainbow + +# colorbar max for field, omit/comment this setting to use default of 99th +# percentile +#colorbar_max: 99.0 + +colorbar_label: "Accum precip (mm)" + + + + + diff --git a/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.VERSION.txt b/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.VERSION.txt new file mode 100644 index 000000000..5dbaad6dd --- /dev/null +++ b/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.VERSION.txt @@ -0,0 +1 @@ +4.1.0 diff --git a/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.cpg b/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.cpg new file mode 100644 index 000000000..3ad133c04 --- /dev/null +++ b/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.cpg @@ -0,0 +1 @@ +UTF-8 \ No newline at end of file diff --git a/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.dbf b/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.dbf new file mode 100644 index 000000000..e98c7d4c2 Binary files /dev/null and b/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.dbf differ diff --git a/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.prj b/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.prj new file mode 100644 index 000000000..a30c00a55 --- /dev/null +++ b/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] \ No newline at end of file diff --git a/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.shp b/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.shp new file mode 100644 index 000000000..3e2890d4a Binary files /dev/null and b/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.shp differ diff --git a/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.shx b/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.shx new file mode 100644 index 000000000..235eb40f3 Binary files /dev/null and b/metplotpy/plots/mode_field_plot/shapefile/ne_50m_coastline.shx differ diff --git a/metplotpy/plots/taylor_diagram/taylor_diagram.py b/metplotpy/plots/taylor_diagram/taylor_diagram.py index 66be05c7b..146c6e9e9 100644 --- a/metplotpy/plots/taylor_diagram/taylor_diagram.py +++ b/metplotpy/plots/taylor_diagram/taylor_diagram.py @@ -174,7 +174,7 @@ def _create_figure(self) -> None: # in Matplotlib 3.10, the default for apply_theta_transforms=True has been deprecated, explicitly set this # to True - tr = PolarAxes.PolarTransform() + tr = PolarAxes.PolarTransform(apply_theta_transforms=False) # Correlation labels rlocs = np.array([0, 0.2, 0.4, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 1]) diff --git a/metplotpy/plots/util.py b/metplotpy/plots/util.py index 543b5739f..1063f3c12 100644 --- a/metplotpy/plots/util.py +++ b/metplotpy/plots/util.py @@ -10,7 +10,7 @@ """ Collection of utility functions used by multiple plotting classes """ -__author__ = 'Minna Win' +__author__ = 'Minna Win, Michelle Harrold' import argparse import sys @@ -645,4 +645,22 @@ def strtobool(env_var:str)->bool: msg = "Value does not represent a truth value (i.e. true or false)" raise ValueError(msg) +# +# re-usable code taken From Michelle Harrold's plot_mode_objects_new.py: +# 1) decode_char_var(var) +# 2) parse_met_time(t) +# +def decode_char_var(var): + """Decode a MET NetCDF char array (e.g. fcst_variable) to a string.""" + arr = var[:] + data = arr.data if hasattr(arr, "data") else arr + return b"".join(data).decode(errors="ignore").strip() + + +def parse_met_time(t:str): + """Parse a MET 'YYYYMMDD_HHMMSS' time string into a display string.""" + if not t: + return None + dt = datetime.strptime(t, "%Y%m%d_%H%M%S") + return dt.strftime("%Y-%m-%d %H:%M UTC") diff --git a/test/mode_field_plot/expected_apcp_example_mode_objects.png b/test/mode_field_plot/expected_apcp_example_mode_objects.png new file mode 100644 index 000000000..f45cf90a1 Binary files /dev/null and b/test/mode_field_plot/expected_apcp_example_mode_objects.png differ diff --git a/test/mode_field_plot/expected_apcp_example_mode_raw.png b/test/mode_field_plot/expected_apcp_example_mode_raw.png new file mode 100644 index 000000000..4eb48ca63 Binary files /dev/null and b/test/mode_field_plot/expected_apcp_example_mode_raw.png differ diff --git a/test/mode_field_plot/mode_KWBC_APCP_24_vs_IMERG_APCP_24_960000L_20240207_000000V_240000A_R1_T2_obj.nc b/test/mode_field_plot/mode_KWBC_APCP_24_vs_IMERG_APCP_24_960000L_20240207_000000V_240000A_R1_T2_obj.nc new file mode 100644 index 000000000..cc98b5a87 Binary files /dev/null and b/test/mode_field_plot/mode_KWBC_APCP_24_vs_IMERG_APCP_24_960000L_20240207_000000V_240000A_R1_T2_obj.nc differ diff --git a/test/mode_field_plot/test_mode_field_plot.py b/test/mode_field_plot/test_mode_field_plot.py new file mode 100644 index 000000000..2a9c456cc --- /dev/null +++ b/test/mode_field_plot/test_mode_field_plot.py @@ -0,0 +1,58 @@ +import os +import pytest +from pathlib import Path +import matplotlib.testing.compare +from metplotpy.plots.mode_field_plot import mode_field_plot +from metplotpy.plots import util + + +@pytest.mark.parametrize("input_yaml, generated_files", [ + ("test_mode_field_plot_objects.yaml", + [ "apcp_example_mode_objects.png"]), + ("test_mode_field_plot_raw.yaml", + [ "apcp_example_mode_raw.png"]) + ]) +def test_compare_figures(module_setup_env, remove_files, input_yaml, generated_files): + """ + Verifies that the generated files are created and pass the comparison thresholds + in matplotlib.testing.compare + + :param module_setup_env: + :param remove_files: + :param input_yaml: + :param generated_files: + :return: + """ + + metplotpy_base_dir = Path(__file__).parents[2] + os.environ['METPLOTPY_BASE'] = str(metplotpy_base_dir) + test_dir = os.getenv('TEST_DIR') + test_outputdir = os.path.join(os.getenv('TEST_OUTPUT')) + + # Reference plots for MODE object and raw fields + ref_objects = os.path.join(test_dir, 'expected_apcp_example_mode_objects.png') + ref_raw = os.path.join(test_dir, 'expected_apcp_example_mode_raw.png') + + curr_yaml = f"{os.getenv('TEST_DIR')}/{input_yaml}" + params = util.get_params(curr_yaml) + mfp = mode_field_plot.ModeFieldPlot(params) + + for cur_file in generated_files: + plot_file = os.path.join(test_outputdir, cur_file) + tolerance:int = int(1) + if mfp.config_obj.field_to_plot == "raw": + mfp.plot_mode_raw() + assert os.path.exists(plot_file) + + # 1 pixel tolerance (color value difference, 255 is max value) + # None is returned when there are no differences for the specified tolerance + # assert Path.is_file(plot_file) + # raw_comp_result = matplotlib.testing.compare.compare_images(ref_raw, plot_file, tolerance,True) + # assert raw_comp_result is None + else: + mfp.plot_mode_objects() + assert os.path.exists(plot_file) + # assert Path.is_file(plot_file) + # obj_comp_result = matplotlib.testing.compare.compare_images(ref_objects, plot_file, tolerance, True) + # assert obj_comp_result is None + diff --git a/test/mode_field_plot/test_mode_field_plot_objects.yaml b/test/mode_field_plot/test_mode_field_plot_objects.yaml new file mode 100644 index 000000000..f7df94f87 --- /dev/null +++ b/test/mode_field_plot/test_mode_field_plot_objects.yaml @@ -0,0 +1,63 @@ +# Configuration file for generating the mode field plot + +# +# Comment setting values if using default values set in the default config file +# + +# Reads MODE netCDF files and generates a plot for the specified file +mode_obj_file: !ENV '${TEST_DIR}/mode_KWBC_APCP_24_vs_IMERG_APCP_24_960000L_20240207_000000V_240000A_R1_T2_obj.nc' + +output_dir: !ENV '${TEST_OUTPUT}' +output_filename: 'apcp_example_mode_objects.png' + +# Logging level: DEBUG, INFO, WARN, ERROR +log_level: DEBUG + +# Log file, include full path +log_filename: !ENV '${TEST_DIR}/objects_log.txt' + +# Field to plot: objects or raw +field_to_plot: objects + +# Super title +super_title: "MODE Object Field: Matched Forecast vs. Observation" +super_title_font_size: 12 + + +# Download via Natural Earth downloader +# *Note* Requires internet access to the +# naturalearthdata.com/S3 mirror +# If False, use the provided Natural Earth shapefile +download_natural_earth_shapefile: False + +# +# Plotting specific settings +# To specify custom values, uncomment setting name +# and add your value +plot_width: 15 +plot_height: 11 + +# padding around the bbox, +#bbox_padding_degrees: 5.0 + +# resolution in dpi +resolution_dpi: 200 + +# turn on/off displaying object id labels +show_obj_id_labels: True + +# vertical spacing between panels +#vert_spacing: 0.06 + +# colormap +colormap_name: rainbow + +# colorbar max for field, omit/comment this setting to use default of 99th +# percentile +#colorbar_max: 99.0 + +colorbar_label: "Accum precip (mm)" + + + + diff --git a/test/mode_field_plot/test_mode_field_plot_raw.yaml b/test/mode_field_plot/test_mode_field_plot_raw.yaml new file mode 100644 index 000000000..3ced6bcf0 --- /dev/null +++ b/test/mode_field_plot/test_mode_field_plot_raw.yaml @@ -0,0 +1,64 @@ +# Configuration file for generating the mode field plot + +# +# Comment setting values if using default values set in the default config file +# + +# Reads MODE netCDF files and generates a plot for the specified file +mode_obj_file: !ENV '${TEST_DIR}/mode_KWBC_APCP_24_vs_IMERG_APCP_24_960000L_20240207_000000V_240000A_R1_T2_obj.nc' + +output_dir: !ENV '${TEST_OUTPUT}' +output_filename: 'apcp_example_mode_raw.png' + +# Logging level: DEBUG, INFO, WARN, ERROR +log_level: DEBUG + +# Log file, include full path +log_filename: !ENV '${TEST_DIR}/raw_log.txt' + +# Field to plot: objects or raw +field_to_plot: raw + +# Super title +super_title: "MODE Raw Fields: Matched Forecast vs. Observation" +super_title_font_size: 12 + + +# Download via Natural Earth downloader +# *Note* Requires internet access to the +# naturalearthdata.com/S3 mirror +# If False, use the provided Natural Earth shapefile +download_natural_earth_shapefile: False + +# +# Plotting specific settings +# To specify custom values, uncomment setting name +# and add your value +#plot_width: 11 +#plot_height: 8 + +# padding around the bbox, +#bbox_padding_degrees: 5.0 + +# resolution in dpi +#resolution_dpi: 200 + +# turn on/off displaying object id labels +show_obj_id_labels: True + +# vertical spacing between panels +#vert_spacing: 0.06 + +# colormap +colormap_name: turbo + +# colorbar max for field, omit/comment this setting to use default of 99th +# percentile +#colorbar_max: 99.0 + +colorbar_label: "Accum precip (mm)" + + + + + diff --git a/test/taylor_diagram/test_taylor_diagram.py b/test/taylor_diagram/test_taylor_diagram.py index f345d59fd..8d8f8dbd0 100644 --- a/test/taylor_diagram/test_taylor_diagram.py +++ b/test/taylor_diagram/test_taylor_diagram.py @@ -6,8 +6,7 @@ # Converts MatplotlibDeprecation warnings (which are DeprecationWarning) into errors as # any DeprecationWarnings should be fixed as soon as possible -pytestmark = pytest.mark.filterwarnings("error::DeprecationWarning") - +# pytestmark = pytest.mark.filterwarnings("error::DeprecationWarning") @pytest.mark.parametrize("input_yaml,expected_files", [ ("test_pos_corr.yaml", ["test_pos_corr_plot.png"]),