Source code for Hydrological_model_validator.Plotting.Target_plots

###############################################################################
##                                                                           ##
##                               LIBRARIES                                   ##
##                                                                           ##
###############################################################################

# Standard library imports
import itertools
from itertools import cycle
from pathlib import Path

# Data handling and plotting libraries
import numpy as np
import matplotlib.pyplot as plt
import skill_metrics as sm

# Module formatting and plotting utilities
from Hydrological_model_validator.Plotting.formatting import fill_annular_region, get_variable_label_unit
from Hydrological_model_validator.Plotting.default_target_options import (
    default_target_base_options,
    default_target_overlay_options,
    default_target_data_marker_options,
    default_target_plt_options,
    default_month_markers,
    default_target_monthly_plt_options,
    default_target_monthly_base_options,
    default_target_monthly_data_marker_options,
)

# Local processing modules
from Hydrological_model_validator.Processing.Target_computations import (
    compute_normalised_target_stats,
    compute_normalised_target_stats_by_month,
    compute_target_extent_monthly,
    compute_target_extent_yearly,
)
from Hydrological_model_validator.Processing.data_alignment import extract_mod_sat_keys
from Hydrological_model_validator.Processing.utils import extract_options

###############################################################################
##                                                                           ##
##                               FUNCTIONS                                   ##
##                                                                           ##
###############################################################################


[docs] def comprehensive_target_diagram(data_dict: dict, **kwargs) -> None: """ Generate a comprehensive yearly target diagram using normalized statistics (bias, CRMSD, RMSD). Parameters ---------- data_dict : dict Dictionary of model and reference time series indexed by datetime Other Parameters ---------------- **kwargs Additional keyword arguments include: output_path : str or Path Required. Directory where the figure is saved. variable_name : str Short name used to infer full variable name and unit. variable : str Full variable name (e.g., "Chlorophyll"). unit : str Unit of measurement (e.g., "mg Chl/m³"). filename : str Name of the output image file. title : str Custom title for the plot. zone_bounds : tuple of float Tuple of two floats defining zone radii (e.g., (0.5, 0.7)). marker_shapes : list List of marker shapes for the data points. base_* : various Options for the base target diagram (prefix: "base_"). overlay_* : various Options for the overlay circles (prefix: "overlay_"). data_* : various Options for the data point markers (prefix: "data_"). (no prefix) : various General plot options (e.g., figsize, dpi, title_pad). Raises ------ ValueError If required variable info or output path is missing. Examples -------- >>> comprehensive_target_diagram( ... data_dict, ... variable_name="Chl", ... output_path="./plots", ... base_alpha=0.3, ... overlay_circles=[0.5, 1.0, 1.5], ... data_markersize=16, ... title="Target Plot" ... ) """ # ----- VALIDATE AND PREPARE INPUT OPTIONS ----- output_path_value = kwargs.pop("output_path", None) if output_path_value is None: raise ValueError("output_path must be specified either in kwargs or default options.") output_path = Path(output_path_value) variable_name = kwargs.pop("variable_name", None) variable = kwargs.pop("variable", None) unit = kwargs.pop("unit", None) if variable_name is not None: variable, unit = get_variable_label_unit(variable_name) variable = variable or kwargs.get("variable") unit = unit or kwargs.get("unit") elif variable is None or unit is None: raise ValueError("You must provide either 'variable' and 'unit' or 'variable_name' in kwargs.") # ----- EXTRACT PLOTTING OPTIONS FROM KWARGS ----- base_opts = extract_options(kwargs, default_target_base_options, prefix="base_") overlay_opts = extract_options(kwargs, default_target_overlay_options, prefix="overlay_") data_marker_opts = extract_options(kwargs, default_target_data_marker_options, prefix="data_") plt_opts = extract_options(kwargs, default_target_plt_options) # ----- COMPUTE STATISTICS AND LAYOUT ----- bias, crmsd, rmsd, labels = compute_normalised_target_stats(data_dict) marker_shapes = cycle(kwargs.pop("marker_shapes", ["P", "o", "X", "s", "D", "^", "v", "p", "h", "*"])) extent = compute_target_extent_yearly(data_dict) # ----- INITIALIZE THE FIGURE ----- fig, ax = plt.subplots(figsize=plt_opts.get("figsize"), dpi=plt_opts.get("dpi")) # ----- DRAW PERFORMANCE SHADED REGIONS ----- zone_bounds = kwargs.get("zone_bounds", (0.5, 0.7)) if not (isinstance(zone_bounds, tuple) and len(zone_bounds) == 2 and all(isinstance(x, (int, float)) for x in zone_bounds)): raise ValueError("'zone_bounds' must be a tuple of two numbers, e.g., (0.5, 0.7)") bound1, bound2 = zone_bounds for r_start, r_end, color in [ (0, bound1, 'lightgreen'), (bound1, bound2, 'khaki'), (bound2, extent, 'lightcoral') ]: fill_annular_region(ax, min(r_start, r_end), max(r_start, r_end), color=color, alpha=0.4) ax.set_axisbelow(True) # ----- DRAW BASE DIAGRAM ----- sm.target_diagram(1.0, extent, extent, markerLabelColor=base_opts.get('markerLabelColor'), alpha=base_opts.get('alpha'), markersize=base_opts.get('markersize'), circlelinespec=base_opts.get('circlelinespec')) # ----- DRAW OVERLAY ----- sm.target_diagram(1.0, extent, extent, markerLabelColor=overlay_opts.get('markerLabelColor'), alpha=overlay_opts.get('alpha'), markersize=overlay_opts.get('markersize'), circlelinespec=overlay_opts.get('circlelinespec'), circles=overlay_opts.get('circles')) # ----- PLOT YEARLY DATA POINTS ----- for b, c, r, label in zip(bias, crmsd, rmsd, labels): sm.target_diagram(b, c, r, markerLabelColor=data_marker_opts.get('markerLabelColor'), markersymbol=next(marker_shapes), markersize=data_marker_opts.get('markersize'), alpha=data_marker_opts.get('alpha'), circles=data_marker_opts.get('circles'), overlay=data_marker_opts.get('overlay')) # ----- ADD THE TITLE ----- plt.title(kwargs.get("title", f"Yearly Target Plot (Normalized Stats) | {variable}"), pad=plt_opts.get("title_pad"), fontweight=plt_opts.get("title_fontweight")) # ----- CHECK FOR DIRECTORY EXISTENCE ----- output_path.mkdir(parents=True, exist_ok=True) save_path = output_path / kwargs.get("filename", "Unified_Target_Diagram.png") # ----- PRINT THE PLOT AND SAVE ----- plt.tight_layout() plt.savefig(save_path, dpi=plt_opts.get("dpi")) plt.close()
############################################################################### ###############################################################################
[docs] def target_diagram_by_month(data_dict: dict, **kwargs) -> None: """ Generate a monthly target diagram with normalized statistics (bias, CRMSD, RMSD). Parameters ---------- data_dict : dict Dictionary of model and reference monthly time series by year. Other Parameters ---------------- **kwargs Additional keyword arguments include: output_path : str or Path Required. Directory where the figure is saved. variable_name : str Short name used to infer full variable name and unit. variable : str Full variable name (e.g., "Chlorophyll"). unit : str Unit of measurement (e.g., "mg Chl/m³"). filename : str Name of the output image file. title : str Custom title for the plot. zone_bounds : tuple Tuple of two floats defining zone radii (e.g., (0.5, 0.7)). markers : list List of marker shapes per year. month_colors : list List of colors corresponding to each month. base_* : various Options for the base diagram (prefix: "base_"). overlay_* : various Options for the overlay diagram (prefix: "overlay_"). data_* : various Options for data point markers (prefix: "data_"). (no prefix) : various General plot options (e.g., figsize, dpi, fontsize). Raises ------ ValueError If required variable info or output path is missing. Examples -------- >>> target_diagram_by_month( ... data_dict, ... variable_name="Chl", ... output_path="./monthly_plots", ... data_markersize=16, ... title="Monthly Target Diagram" ... ) """ # ---- VALIDATE AND PREPARE INPUT OPTIONS ---- output_path = kwargs.pop("output_path", None) if output_path is None: raise ValueError("output_path must be specified in kwargs.") output_path = Path(output_path) output_path.mkdir(parents=True, exist_ok=True) variable_name = kwargs.pop("variable_name", None) variable = kwargs.pop("variable", None) unit = kwargs.pop("unit", None) if variable_name: variable, unit = get_variable_label_unit(variable_name) variable = variable or kwargs.get("variable") unit = unit or kwargs.get("unit") elif variable is None or unit is None: raise ValueError("You must provide either 'variable' and 'unit' or 'variable_name' in kwargs.") # ---- EXTRACT PLOTTING OPTIONS ---- markers = itertools.cycle(kwargs.pop("markers", default_month_markers["markers"])) month_colors = itertools.cycle(kwargs.pop("month_colors", default_month_markers["month_colors"])) plt_opts = extract_options(kwargs, default_target_monthly_plt_options) base_opts = extract_options(kwargs, default_target_monthly_base_options, prefix="base_") overlay_opts = extract_options(kwargs, default_target_overlay_options, prefix="overlay_") data_marker_opts = extract_options(kwargs, default_target_monthly_data_marker_options, prefix="data_") # ---- COMPUTE EXTENT TO CORRECTLY ENCOMPASS ALL MARKERS ---- extent = compute_target_extent_monthly(data_dict) # ----- INITIALIZE FIGURE ----- fig, ax = plt.subplots(figsize=plt_opts.get("figsize")) # ----- CREATE TITLE ----- plt.title( kwargs.get("title", f"Monthly Target Plot (Normalized Stats) | {variable}"), fontsize=plt_opts.get("title_fontsize"), pad=plt_opts.get("title_pad"), fontweight=plt_opts.get("title_fontweight") ) # ---- DRAW BASE DIAGRAM ---- sm.target_diagram( np.array([1.0]), np.array([extent]), np.array([1.0]), markerLabelColor=base_opts.get("markerLabelColor"), markersize=base_opts.get("markersize"), alpha=base_opts.get("alpha"), circles=base_opts.get("overlay_circles", [0.7, 1.0] + list(np.arange(2.0, extent + 1e-6, 1.0))), circlelinespec=base_opts.get("circlelinespec") ) # ----- DRAW OVERLAY ----- sm.target_diagram( np.array([1.0]), np.array([extent]), np.array([1.0]), markerLabelColor=overlay_opts.get("markerLabelColor"), markersize=overlay_opts.get("markersize"), alpha=overlay_opts.get("alpha"), circles=overlay_opts.get("circles"), circlelinespec=overlay_opts.get("circlelinespec") ) # ---- DRAW SHADED ZONES ---- zone_bounds = kwargs.get("zone_bounds", (0.5, 0.7)) if not (isinstance(zone_bounds, tuple) and len(zone_bounds) == 2 and all(isinstance(x, (int, float)) for x in zone_bounds)): raise ValueError("'zone_bounds' must be a tuple of two numbers, e.g., (0.5, 0.7)") bound1, bound2 = zone_bounds for r_start, r_end, color in [(0, bound1, 'lightgreen'), (bound1, bound2, 'khaki'), (bound2, extent, 'lightcoral')]: fill_annular_region(ax, min(r_start, r_end), max(r_start, r_end), color=color, alpha=0.4) # ---- PLOT MONTHLY DATA POINTS ---- mod_key, _ = extract_mod_sat_keys(data_dict) years = list(data_dict[mod_key].keys()) # ----- LOOP TO COMPUTE ALL DATA ----- for month_index in range(12): try: bias, crmsd, rmsd, labels = compute_normalised_target_stats_by_month(data_dict, month_index) except ValueError: continue # ----- LOOP FOR ALL YEARS TO FETCH SHAPE/COLOR ----- color = next(month_colors) for i, (b, c, r, label) in enumerate(zip(bias, crmsd, rmsd, labels)): year = str(label) year_index = years.index(year) if year in years else i marker = next(markers) # ----- PLOT THE MARKERS ----- sm.target_diagram( np.array([b]), np.array([c]), np.array([r]), markercolors={"face": color, "edge": data_marker_opts.get("edge_color")}, markersymbol=marker, markersize=data_marker_opts.get("markersize"), alpha=data_marker_opts.get("alpha"), overlay=data_marker_opts.get("overlay"), circles=data_marker_opts.get("circles") ) # ---- FORMATTING OPTIONS ---- ax.tick_params(axis='both', labelsize=plt_opts.get("tick_labelsize")) ax.xaxis.label.set_size(plt_opts.get("axis_labelsize")) ax.yaxis.label.set_size(plt_opts.get("axis_labelsize")) # ----- CHECK EXISTENCE OF SAVE FOLDER ----- output_path.mkdir(parents=True, exist_ok=True) save_path = output_path / kwargs.get("filename", "Monthly_Target_Diagram.png") # ----- PRINT AND SAVE THE PLOT ----- plt.savefig(save_path, dpi=plt_opts.get("dpi")) plt.close()
###############################################################################