Source code for de.de

# -*- coding: utf-8 -*-

"""
de.de
~~~~~~~~~~~
Diagnosing model errors using an efficiency measure based on flow
duration curve and temporal correlation. The efficiency measure can be
visualized by diagnostic polar plots which facilitates decomposing potential
error contributions (dynamic errors vs. constant erros vs. timing errors)
:2021 by Robin Schwemmle.
:license: GNU GPLv3, see LICENSE for more details.
"""

import numpy as np
import matplotlib
from matplotlib import cm
import matplotlib.ticker as mticker
import pandas as pd
import scipy as sp
import scipy.integrate as integrate
import seaborn as sns
matplotlib.use("agg")
import matplotlib.pyplot as plt  # noqa: E402

# RunTimeWarning will not be displayed (division by zeros or NaN values)
np.seterr(divide="ignore", invalid="ignore")

# controlling figure aesthetics
sns.set_style("ticks", {"xtick.major.size": 8, "ytick.major.size": 8})


[docs]def calc_brel(obs, sim, sort=True): r""" Calculate relative bias. Parameters ---------- obs : (N,)array_like observed time series as 1-D array sim : (N,)array_like simulated time series as 1-D array sort : boolean, optional If True, time series are sorted by ascending order. If False, time series are not sorted. The default is to sort. Returns ---------- brel : (N,)array_like relative bias Notes ---------- .. math:: B_{rel} = \frac{Q_{sim}(i) - Q_{obs}(i)}{Q_{obs}(i)} Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> brel = de.calc_brel(obs, sim) """ if len(obs) != len(sim): raise AssertionError("Arrays are not of equal length!") if sort: obs = np.sort(obs)[::-1] sim = np.sort(sim)[::-1] sim_obs_diff = np.subtract(sim, obs) brel = np.divide(sim_obs_diff, obs) brel[sim_obs_diff == 0] = 0 brel = brel[np.isfinite(brel)] return brel
[docs]def calc_brel_mean(obs, sim, sort=True): r""" Calculate the arithmetic mean of the relative bias. Parameters ---------- obs : (N,)array_like observed time series as 1-D array sim : (N,)array_like simulated time series as 1-D array sort : boolean, optional If True, time series are sorted by ascending order. If False, time series are not sorted. The default is to sort. Returns ---------- brel_mean : float average relative bias Notes ---------- .. math:: \overline{B_{rel}} = \frac{1}{N}\sum_{i=1}^{N} B_{rel}(i) Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> de.calc_brel_mean(obs, sim) 0.09330065359477124 """ if len(obs) != len(sim): raise AssertionError("Arrays are not of equal length!") if sort: obs = np.sort(obs)[::-1] sim = np.sort(sim)[::-1] sim_obs_diff = np.subtract(sim, obs) brel = np.divide(sim_obs_diff, obs) brel[sim_obs_diff == 0] = 0 brel = brel[np.isfinite(brel)] brel_mean = np.mean(brel) # set numerical artefacts to zero if abs(brel_mean) < 0.001: brel_mean = 0 return brel_mean
[docs]def calc_brel_res(obs, sim, sort=True): r""" Subtracting arithmetic mean of the relative bias from the relative bias. Parameters ---------- obs : (N,)array_like observed time series as 1-D array sim : (N,)array_like simulated time series sort : boolean, optional If True, time series are sorted by ascending order. If False, time series are not sorted. The default is to sort. Returns ---------- brel_res : (N,)array_like remaining relative bias Notes ---------- .. math:: B_{res}(i) = B_{rel}(i) - \overline{B_{rel}} Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> de.calc_brel_res(obs, sim) array([ 0.15669935, -0.02663399, -0.22663399, 0.10669935, 0.08316993, -0.09330065]) """ if len(obs) != len(sim): raise AssertionError("Arrays are not of equal length!") if sort: obs = np.sort(obs)[::-1] sim = np.sort(sim)[::-1] sim_obs_diff = np.subtract(sim, obs) brel = np.divide(sim_obs_diff, obs) brel[sim_obs_diff == 0] = 0 brel = brel[np.isfinite(brel)] brel_mean = np.mean(brel) brel_res = brel - brel_mean return brel_res
[docs]def calc_bias_area(brel_res): r""" Calculate the integrated residual bias for the entire flow duration curve. Parameters ---------- brel_res : (N,)array_like remaining relative bias as 1-D array Returns ---------- b_area : float bias area Notes ---------- .. math:: \vert B_{area}\vert = \int_{0}^{1}\vert B_{res}(i)\vert di Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> b_res = de.calc_brel_res(obs, sim) >>> de.calc_bias_area(b_res) 0.1112908496732026 See Also -------- de.calc_brel_res """ perc = np.linspace(0, 1, len(brel_res)) # area of absolute bias b_area = integrate.simps(abs(brel_res), perc) # set numerical artefacts to zero if abs(b_area) < 0.001: b_area = 0 return b_area
[docs]def calc_bias_tot(brel): r""" Calculate the integrated relative bias for the entire flow duration curve. Parameters ---------- brel : (N,)array_like relative bias as 1-D array Returns ---------- b_tot : float bias area of the entire flow domain Notes ---------- .. math:: \vert B_{area}\vert = \int_{0}^{1}\vert B_{rel}(i)\vert di Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> b_rel = de.calc_brel(obs, sim) >>> de.calc_bias_tot(b_rel) 0.14017973856209148 See Also -------- de.calc_brel """ perc = np.linspace(0, 1, len(brel)) # area of absolute bias brel_abs = np.abs(brel) b_tot = integrate.simps(brel_abs, perc) # set numerical artefacts to zero if abs(b_tot) < 0.001: b_tot = 0 return b_tot
[docs]def calc_bias_hf(brel): r""" Calculate the integrated relative bias for high flows. Parameters ---------- brel : (N,)array_like relative bias as 1-D array Returns ---------- b_hf : float absolute bias area of high flows (i.e. 0th percentile to 50th percentile) Notes ---------- .. math:: B_{hf} = \int_{0}^{0.5}B_{rel}(i) di See Also -------- de.calc_brel Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> b_rel = de.calc_brel(obs, sim) >>> de.calc_bias_hf(b_rel) 0.031944444444444456 """ mid_idx = int(len(brel) / 2) # integral of relative bias < 50 % n = len(brel[:mid_idx]) perc_hf = np.linspace(0, 0.5, n) # direction of bias from high flows b_hf = integrate.simps(brel[:mid_idx], perc_hf) # set numerical artefacts to zero if abs(b_hf) < 0.001: b_hf = 0 return b_hf
[docs]def calc_err_hf(b_hf, b_tot): r""" Calculate the error contribution of high flows. Parameters ---------- b_hf : float absolute bias area of high flows (i.e. 0th percentile to 50th percentile) b_tot : float bias area of the entire flow domain Returns ---------- err_hf : float contribution of high flows to model error Notes ---------- .. math:: \epsilon_{hf} = \frac{B_{hf}}{B_{tot}} See Also -------- de.calc_bias_hf and de.calc_bias_tot Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> b_rel = de.calc_brel(obs, sim) >>> b_hf = de.calc_bias_hf(b_rel) >>> b_tot = de.calc_bias_tot(b_rel) >>> de.calc_err_hf(b_hf, b_tot) 0.2278820375335122 """ if b_tot > 0: err_hf = b_hf / b_tot else: err_hf = 0 # set nan to zero if err_hf == np.nan: err_hf = 0 return err_hf
[docs]def calc_bias_lf(brel): r""" Calculate the integrated relative for low flows. Parameters ---------- brel : (N,)array_like relative bias as 1-D array Returns ---------- b_lf : float absolute bias area of low flows (i.e. 50th percentile to 100th percentile) Notes ---------- .. math:: B_{lf} = \int_{0.5}^{1}B_{rel}(i) di Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> b_rel = de.calc_brel(obs, sim) >>> de.calc_bias_lf(b_rel) 0.07549019607843138 """ mid_idx = int(len(brel) / 2) # integral of relative bias < 50 % n = len(brel[mid_idx:]) perc_low = np.linspace(0.5, 1, n) # direction of bias from high flows b_lf = integrate.simps(brel[mid_idx:], perc_low) # set numerical artefacts to zero if abs(b_lf) < 0.001: b_lf = 0 return b_lf
[docs]def calc_err_lf(b_lf, b_tot): r""" Calculate the error contribution of low flows. Parameters ---------- b_lf : float absolute bias area of low flows (i.e. 50th percentile to 100th percentile) b_tot : float bias area of the entire flow domain Returns ---------- err_lf : float contribution of low flows to dynamic error Notes ---------- .. math:: \epsilon_{lf} = \frac{B_{lf}}{B_{tot}} See Also -------- de.calc_bias_hf and de.calc_bias_tot Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> b_rel = de.calc_brel(obs, sim) >>> b_lf = de.calc_bias_lf(b_rel) >>> b_tot = de.calc_bias_tot(b_rel) >>> de.calc_err_lf(b_lf, b_tot) 0.5385243035318803 """ if b_tot > 0: err_lf = b_lf / b_tot else: err_lf = 0 # set nan to zero if err_lf == np.nan: err_lf = 0 return err_lf
[docs]def calc_bias_dir(brel_res): r""" Calculate the direction of the dynamic error. Parameters ---------- brel_res : (N,)array_like remaining relative bias Returns ---------- b_dir : float direction of bias Notes ---------- .. math:: B_{dir} = \begin{cases} -1 & \text{if } (B_{res-hf} > 0 & B_{lf} < 0) | (B_{res-hf} = 0 & B_{res-lf} < 0) | (B_{res-hf} > 0 & B_{res-lf} = 0) \\ 1 & \text{if } (B_{res-hf} < 0 & B_{lf} > 0) | (B_{res-hf} = 0 & B_{res-lf} > 0) | (B_{res-hf} < 0 & B_{res-lf} = 0) \\ 0 & \text{if } (B_{res-hf} > 0 & B_{lf} > 0) | (B_{res-hf} < 0 & B_{res-lf} < 0) | (B_{res-hf} = 0 & B_{res-lf} = 0) \end{cases} Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> brel_res = de.calc_brel_res(obs, sim) >>> de.calc_bias_dir(brel_res) 1 """ b_res_hf = calc_bias_hf(brel_res) b_res_lf = calc_bias_lf(brel_res) if (b_res_hf > 0 and b_res_lf < 0) or (b_res_hf == 0 and b_res_lf < 0) or (b_res_hf > 0 and b_res_lf == 0): b_dir = -1 elif (b_res_hf < 0 and b_res_lf > 0) or (b_res_hf == 0 and b_res_lf > 0) or (b_res_hf < 0 and b_res_lf == 0): b_dir = 1 elif (b_res_hf > 0 and b_res_lf > 0) or (b_res_hf < 0 and b_res_lf < 0) or (b_res_hf == 0 and b_res_lf == 0): b_dir = 0 return b_dir
[docs]def calc_bias_slope(b_area, b_dir): r""" Calculate the slope of the residual bias. Parameters ---------- b_area : float absolute area of residual bias b_dir : float direction of bias Returns ---------- b_slope : float slope of bias Notes ---------- .. math:: B_{slope} = \vert B_{area}\vert \times B_{dir} See Also -------- de.calc_bias_area and de.calc_bias_dir Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> brel_res = de.calc_brel(obs, sim) >>> b_dir = de.calc_bias_dir(brel_res) >>> b_area = de.calc_bias_area(b_res) >>> de.calc_bias_slope(b_area, b_dir) 0.11 """ b_slope = b_area * b_dir return b_slope
[docs]def calc_temp_cor(obs, sim, r="pearson"): """ Calculate temporal correlation between observed and simulated time series. Parameters ---------- obs : (N,)array_like Observed time series as 1-D array sim : (N,)array_like Simulated time series r : str, optional Either Spearman correlation coefficient ('spearman') or Pearson correlation coefficient ('pearson') can be used to describe the temporal correlation. The default is to calculate the Pearson correlation. Returns ---------- temp_cor : float correlation between observed and simulated time series Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> de.calc_temp_cor(obs, sim) 0.89 """ if len(obs) != len(sim): raise AssertionError("Arrays are not of equal length!") if r == "spearman": r = sp.stats.spearmanr(obs, sim) temp_cor = r[0] if np.isnan(temp_cor): temp_cor = 0 elif r == "pearson": r = sp.stats.pearsonr(obs, sim) temp_cor = r[0] if np.isnan(temp_cor): temp_cor = 0 return temp_cor
[docs]def calc_de(obs, sim, sort=True): r""" Calculate Diagnostic-Efficiency (DE). Parameters ---------- obs : (N,)array_like Observed time series as 1-D array sim : (N,)array_like Simulated time series sort : boolean, optional If True, time series are sorted by ascending order. If False, time series are not sorted. The default is to sort. Returns ---------- eff : float Diagnostic efficiency Notes ---------- .. math:: DE = \sqrt{\overline{B_{rel}}^2 + \vert B_{area}\vert^2 + (r - 1)^2} Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> de.calc_de(obs, sim) 0.18 """ if len(obs) != len(sim): raise AssertionError("Arrays are not of equal length!") # mean relative bias brel_mean = calc_brel_mean(obs, sim, sort=sort) # remaining relative bias brel_res = calc_brel_res(obs, sim, sort=sort) # area of relative remaing bias b_area = calc_bias_area(brel_res) # temporal correlation temp_cor = calc_temp_cor(obs, sim) # diagnostic efficiency eff = np.sqrt((brel_mean) ** 2 + (b_area) ** 2 + (temp_cor - 1) ** 2) return eff
[docs]def calc_phi(brel_mean, b_slope): """ Calculate trigonometric inverse tangent. Parameters ---------- brel_mean : float average relative bias b_slope : float slope of bias Returns ---------- phi : float trigonometric inverse tangent Notes ---------- .. math:: \varphi = arctan2(\overline{B_{rel}}, B_{slope}) Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> brel_mean = de.calc_brel_mean(obs, sim) >>> brel_res = de.calc_brel(obs, sim) >>> b_dir = de.calc_bias_dir(brel_res) >>> b_area = de.calc_bias_area(brel_res) >>> b_slope = de.calc_bias_slope(b_area, b_dir) >>> de.calc_phi(brel_mean, b_slope) 1.5707963267948966 """ phi = np.arctan2(brel_mean, b_slope) # set numerical artefacts to zero if abs(phi) < 0.001: phi = 0 # set numerical artefacts pi if phi > 3.1414: phi = 3.1414 return phi
[docs]def diag_polar_plot(obs, sim, sort=True, limit=0.05, extended=False): r""" Diagnostic polar plot of Diagnostic efficiency (DE) for a single evaluation. Parameters ---------- obs : (N,)array_like Observed time series as 1-D array sim : (N,)array_like Simulated time series as 1-D array sort : boolean, optional If True, time series are sorted by ascending order. If False, time series are not sorted. The default is to sort. limit : float, optional Threshold for which diagnosis can be made. The default is 0.05. extended : boolean, optional If True, extended diagnostic plot is displayed. In addtion, the duration curve of B_rest is plotted besides the polar plot. The default is, that only the diagnostic polar plot is displayed. Returns ---------- fig : Figure Returns a single figure if extended=False and two figures if extended=True. Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> obs = np.array([1.5, 1, 0.8, 0.85, 1.5, 2]) >>> sim = np.array([1.6, 1.3, 1, 0.8, 1.2, 2.5]) >>> de.diag_polar_plot(obs, sim) """ if len(obs) != len(sim): raise AssertionError("Arrays are not of equal length!") # relative bias brel = calc_brel(obs, sim, sort=sort) # mean relative bias brel_mean = calc_brel_mean(obs, sim, sort=sort) # residual relative bias brel_res = calc_brel_res(obs, sim, sort=sort) # area of relative remaing bias b_area = calc_bias_area(brel_res) # temporal correlation temp_cor = calc_temp_cor(obs, sim) # diagnostic efficiency eff = np.sqrt((brel_mean) ** 2 + (b_area) ** 2 + (temp_cor - 1) ** 2) # direction of bias b_dir = calc_bias_dir(brel_res) # slope of bias b_slope = calc_bias_slope(b_area, b_dir) # total bias b_tot = calc_bias_tot(brel) # bias of high flows b_hf = calc_bias_hf(brel) # bias of low flows b_lf = calc_bias_lf(brel) # contribution of high flows to dyn. error err_hf = calc_err_hf(b_hf, b_tot) # contribution of low flows to dyn. error err_lf = calc_err_lf(b_lf, b_tot) # convert to radians # (y, x) Trigonometric inverse tangent phi = calc_phi(brel_mean, b_slope) # convert temporal correlation to color norm = matplotlib.colors.Normalize(vmin=0, vmax=1.0) cmap = cm.get_cmap('plasma_r') rgba_color = cmap(norm(temp_cor)) delta = 0.01 # for spacing # determine axis limits if eff <= 1: ax_lim = 1.2 yy = np.arange(0.01, ax_lim, delta) c_levels = np.arange(0, ax_lim, 0.2) elif eff > 1 and eff <= 2: ax_lim = 2.2 yy = np.arange(0.01, ax_lim, delta) c_levels = np.arange(0, ax_lim, 0.2) elif eff > 2 and eff <= 3: ax_lim = 3.2 yy = np.arange(0.01, ax_lim, delta) c_levels = np.arange(0, ax_lim, 0.2) elif eff >= 3: raise AssertionError("Value of 'DE' is out of bounds for visualization!", eff) len_yy = len(yy) # arrays to plot contour lines of DE xx = np.radians(np.linspace(0, 360, len_yy)) theta, r = np.meshgrid(xx, yy) # diagnostic polar plot if not extended: fig, ax = plt.subplots( figsize=(3, 3), subplot_kw=dict(projection="polar"), constrained_layout=True ) # dummie plot for colorbar of temporal correlation cs = np.arange(0, 1.1, 0.1) dummie_cax = ax.scatter(cs, cs, c=cs, cmap="plasma_r") # Clear axis ax.cla() # plot regions ax.plot( (0, np.deg2rad(45)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(135)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(225)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(315)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) # contours of DE cp = ax.contour(theta, r, r, colors="darkgray", levels=c_levels, zorder=1, linewidths=1) cl = ax.clabel(cp, inline=True, fmt="%1.1f", colors="dimgrey") # threshold efficiency for FBM eff_l = np.sqrt((limit) ** 2 + (limit) ** 2 + (limit) ** 2) # relation of high flow errors and low flow errors which explain # the total FDC error if b_tot > 0: exp_err = (abs(b_hf) + abs(b_lf)) / b_tot elif b_tot == 0: exp_err = 0 # calculate pies to display error contribution of high flows and low # flows # calculate the points of the first pie marker # these are just the origin (0, 0) + some (cos, sin) points on a circle r1 = abs(err_hf)/2 x1 = np.cos(2 * np.pi * np.linspace(0.25 - r1/2, 0.25 + r1/2)) y1 = np.sin(2 * np.pi * np.linspace(0.25 - r1/2, 0.25 + r1/2)) xy1 = np.row_stack([[0, 0], np.column_stack([x1, y1])]) s1 = np.abs(xy1).max() r2 = abs(err_lf)/2 x2 = np.cos(2 * np.pi * np.linspace(0.75 - r2/2, 0.75 + r2/2)) y2 = np.sin(2 * np.pi * np.linspace(0.75 - r2/2, 0.75 + r2/2)) xy2 = np.row_stack([[0, 0], np.column_stack([x2, y2])]) s2 = np.abs(xy2).max() # diagnose the error if abs(brel_mean) <= limit and exp_err > limit and eff > eff_l: c0 = ax.scatter(phi, eff, marker=xy1, s=s1 * 50, facecolor=rgba_color, zorder=2) c1 = ax.scatter(phi, eff, marker=xy2, s=s2 * 50, facecolor=rgba_color, zorder=2) c = ax.scatter(phi, eff, s=4, facecolor=rgba_color, zorder=2) c2 = ax.scatter(phi, eff, s=1, facecolor='grey', zorder=2) elif abs(brel_mean) > limit and exp_err <= limit and eff > eff_l: c0 = ax.scatter(phi, eff, marker=xy1, s=s1 * 50, facecolor=rgba_color, zorder=2) c1 = ax.scatter(phi, eff, marker=xy2, s=s2 * 50, facecolor=rgba_color, zorder=2) c = ax.scatter(phi, eff, s=4, facecolor=rgba_color, zorder=2) c2 = ax.scatter(phi, eff, s=1, facecolor='grey', zorder=2) elif abs(brel_mean) > limit and exp_err > limit and eff > eff_l: c0 = ax.scatter(phi, eff, marker=xy1, s=s1 * 50, facecolor=rgba_color, zorder=2) c1 = ax.scatter(phi, eff, marker=xy2, s=s2 * 50, facecolor=rgba_color, zorder=2) c = ax.scatter(phi, eff, s=4, facecolor=rgba_color, zorder=2) c2 = ax.scatter(phi, eff, s=1, facecolor='grey', zorder=2) # FBM elif abs(brel_mean) <= limit and exp_err <= limit and eff > eff_l: ax.annotate( "", xytext=(0, 0), xy=(0, eff), arrowprops=dict(facecolor=rgba_color), zorder=2 ) ax.annotate( "", xytext=(0, 0), xy=(np.pi, eff), arrowprops=dict(facecolor=rgba_color), zorder=2 ) # FGM elif abs(brel_mean) <= limit and exp_err <= limit and eff <= eff_l: c = ax.scatter(phi, eff, s=4, facecolor=rgba_color, zorder=2) c2 = ax.scatter(phi, eff, s=1, facecolor='grey', zorder=2) # legend for error contribution of high flows and low flows rl1 = 1/2 xl1 = np.cos(2 * np.pi * np.linspace(0.25 - rl1/2, 0.25 + rl1/2)) yl1 = np.sin(2 * np.pi * np.linspace(0.25 - rl1/2, 0.25 + rl1/2)) xyl1 = np.row_stack([[0, 0], np.column_stack([xl1, yl1])]) sl1 = np.abs(xyl1).max() rl2 = 1/2 xl2 = np.cos(2 * np.pi * np.linspace(0.75 - rl2/2, 0.75 + rl2/2)) yl2 = np.sin(2 * np.pi * np.linspace(0.75 - rl2/2, 0.75 + rl2/2)) xyl2 = np.row_stack([[0, 0], np.column_stack([xl2, yl2])]) sl2 = np.abs(xyl2).max() ax.scatter([], [], color='k', zorder=2, marker=xyl1, s=sl1 * 50, label=r'high values ($\epsilon_{hf}=1$)') ax.scatter([], [], color='k', zorder=2, marker=xyl2, s=sl2 * 50, label=r'low values ($\epsilon_{lf}=1$)') ax.legend(loc='upper right', title="Error contribution of", fancybox=False, frameon=False, bbox_to_anchor=(1.45, 1.2), handletextpad=0.2) ax.set_rticks([]) # turn default ticks off ax.set_rmin(0) if eff <= 1: ax.set_rmax(1) elif eff > 1: ax.set_rmax(ax_lim) # turn labels and grid off ax.tick_params( labelleft=False, labelright=False, labeltop=False, labelbottom=True, grid_alpha=0.01, ) ticks_loc = ax.get_xticks().tolist() ax.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) ax.set_xticklabels(["", "", "", "", "", "", "", ""]) ax.text( -0.16, 0.5, "High value overestimation -", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( -0.11, 0.5, "Low value underestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( -0.06, 0.5, r"$B_{slope}$ < 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.16, 0.5, "High value underestimation -", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.11, 0.5, "Low value overestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.06, 0.5, r"$B_{slope}$ > 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.12, "Constant negative offset", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.06, r"$\overline{B_{rel}}$ < 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.12, "Constant positive offset", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.06, r"$\overline{B_{rel}}$ > 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) # add colorbar for temporal correlation cbar = fig.colorbar( dummie_cax, ax=ax, orientation="horizontal", ticks=[1, 0.5, 0], shrink=0.8 ) cbar.set_label("r [-]\nTiming error", labelpad=4) cbar.set_ticklabels(["1", "0.5", "<0"]) cbar.ax.tick_params(direction="in") if (obs == 0).any(): N_obs0 = np.sum(obs == 0) N_sim0 = np.sum(sim == 0) share0 = N_obs0 / obs.size a0 = np.round(1 - (N_sim0 / N_obs0), 2) ax.text( 1.4, 0.1, f"Share of zero values [-]: {share0}\n Agreement of zero values [-]: {a0}", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) return fig elif extended: fig = plt.figure(figsize=(6, 3), constrained_layout=True) gs = fig.add_gridspec(1, 2) ax = fig.add_subplot(gs[0, 0], projection="polar") ax1 = fig.add_axes([0.65, 0.3, 0.33, 0.33], frameon=True) # dummie plot for colorbar of temporal correlation cs = np.arange(0, 1.1, 0.1) dummie_cax = ax.scatter(cs, cs, c=cs, cmap="plasma_r") # Clear axis ax.cla() # plot regions ax.plot( (0, np.deg2rad(45)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(135)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(225)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(315)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) # contours of DE cp = ax.contour(theta, r, r, colors="darkgray", levels=c_levels, zorder=1, linewidths=1) cl = ax.clabel(cp, inline=True, fmt="%1.1f", colors="dimgrey") # threshold efficiency for FBM eff_l = np.sqrt((limit) ** 2 + (limit) ** 2 + (limit) ** 2) # relation of high flow errors and low flow errors which explain # the total FDC error if b_tot > 0: exp_err = (abs(b_hf) + abs(b_lf)) / b_tot elif b_tot == 0: exp_err = 0 # calculate pies to display error contribution of high flows and low # flows # calculate the points of the first pie marker # these are just the origin (0, 0) + some (cos, sin) points on a circle r1 = abs(err_hf)/2 x1 = np.cos(2 * np.pi * np.linspace(0.25 - r1/2, 0.25 + r1/2)) y1 = np.sin(2 * np.pi * np.linspace(0.25 - r1/2, 0.25 + r1/2)) xy1 = np.row_stack([[0, 0], np.column_stack([x1, y1])]) s1 = np.abs(xy1).max() r2 = abs(err_lf)/2 x2 = np.cos(2 * np.pi * np.linspace(0.75 - r2/2, 0.75 + r2/2)) y2 = np.sin(2 * np.pi * np.linspace(0.75 - r2/2, 0.75 + r2/2)) xy2 = np.row_stack([[0, 0], np.column_stack([x2, y2])]) s2 = np.abs(xy2).max() # diagnose the error if abs(brel_mean) <= limit and exp_err > limit and eff > eff_l: c0 = ax.scatter(phi, eff, marker=xy1, s=s1 * 50, facecolor=rgba_color, zorder=3) c1 = ax.scatter(phi, eff, marker=xy2, s=s2 * 50, facecolor=rgba_color, zorder=3) c = ax.scatter(phi, eff, s=4, facecolor=rgba_color, zorder=3) c2 = ax.scatter(phi, eff, s=1, facecolor='grey', zorder=3) elif abs(brel_mean) > limit and exp_err <= limit and eff > eff_l: c0 = ax.scatter(phi, eff, marker=xy1, s=s1 * 50, facecolor=rgba_color, zorder=3) c1 = ax.scatter(phi, eff, marker=xy2, s=s2 * 50, facecolor=rgba_color, zorder=3) c = ax.scatter(phi, eff, s=4, facecolor=rgba_color, zorder=3) c2 = ax.scatter(phi, eff, s=1, facecolor='grey', zorder=3) elif abs(brel_mean) > limit and exp_err > limit and eff > eff_l: c0 = ax.scatter(phi, eff, marker=xy1, s=s1 * 50, facecolor=rgba_color, zorder=3) c1 = ax.scatter(phi, eff, marker=xy2, s=s2 * 50, facecolor=rgba_color, zorder=3) c = ax.scatter(phi, eff, s=4, facecolor=rgba_color, zorder=3) c2 = ax.scatter(phi, eff, s=1, facecolor='grey', zorder=3) # FBM elif abs(brel_mean) <= limit and exp_err <= limit and eff > eff_l: ax.annotate( "", xytext=(0, 0), xy=(0, eff), arrowprops=dict(facecolor=rgba_color), zorder=2 ) ax.annotate( "", xytext=(0, 0), xy=(np.pi, eff), arrowprops=dict(facecolor=rgba_color), zorder=2 ) # FGM elif abs(brel_mean) <= limit and exp_err <= limit and eff <= eff_l: c = ax.scatter(phi, eff, color=rgba_color, zorder=3) c2 = ax.scatter(phi, eff, s=1, facecolor='grey', zorder=3) # legend for error contribution of high flows and low flows rl1 = 1/2 xl1 = np.cos(2 * np.pi * np.linspace(0.25 - rl1/2, 0.25 + rl1/2)) yl1 = np.sin(2 * np.pi * np.linspace(0.25 - rl1/2, 0.25 + rl1/2)) xyl1 = np.row_stack([[0, 0], np.column_stack([xl1, yl1])]) sl1 = np.abs(xyl1).max() rl2 = 1/2 xl2 = np.cos(2 * np.pi * np.linspace(0.75 - rl2/2, 0.75 + rl2/2)) yl2 = np.sin(2 * np.pi * np.linspace(0.75 - rl2/2, 0.75 + rl2/2)) xyl2 = np.row_stack([[0, 0], np.column_stack([xl2, yl2])]) sl2 = np.abs(xyl2).max() ax.scatter([], [], color='k', zorder=2, marker=xyl1, s=sl1 * 50, label=r'high values ($\epsilon_{hf}=1$)') ax.scatter([], [], color='k', zorder=2, marker=xyl2, s=sl2 * 50, label=r'low values ($\epsilon_{lf}=1$)') ax.legend(loc='upper right', title="Error contribution of", fancybox=False, frameon=False, bbox_to_anchor=(1.3, 1.1), handletextpad=0.1) ax.set_rticks([]) # turn default ticks off ax.set_rmin(0) if eff <= 1: ax.set_rmax(1) elif eff > 1: ax.set_rmax(ax_lim) # turn labels and grid off ax.tick_params( labelleft=False, labelright=False, labeltop=False, labelbottom=True, grid_alpha=0.01, ) ticks_loc = ax.get_xticks().tolist() ax.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) ax.set_xticklabels(["", "", "", "", "", "", "", ""]) ax.text( -0.16, 0.5, "High value overestimation -", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( -0.11, 0.5, "Low value underestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( -0.04, 0.5, r"$B_{slope}$ < 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.17, 0.5, "High value underestimation -", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.12, 0.5, "Low value overestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.06, 0.5, r"$B_{slope}$ > 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.09, "Constant negative offset", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.04, r"$\overline{B_{rel}}$ < 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.09, "Constant positive offset", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.04, r"$\overline{B_{rel}}$ > 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) # add colorbar for temporal correlation cbar = fig.colorbar( dummie_cax, ax=ax, orientation="horizontal", ticks=[1, 0.5, 0], shrink=0.8 ) cbar.set_label("r [-]\nTiming error", labelpad=4) cbar.set_ticklabels(["1", "0.5", "<0"]) cbar.ax.tick_params(direction="in") # plot residual bias # calculate exceedence probability prob = np.linspace(0, 1, len(brel_res)) ax1.axhline(y=0, color="slategrey") ax1.axvline(x=0.5, color="slategrey") ax1.plot(prob, brel_res, color="black") ax1.fill_between(prob, brel_res, where=0 < brel_res, facecolor="purple") ax1.fill_between(prob, brel_res, where=0 > brel_res, facecolor="red") ax1.set(ylabel=r"$B_{res}$ [-]", xlabel="Exceedence probabilty [-]") return fig
[docs]def diag_polar_plot_multi( brel_mean, temp_cor, eff_de, b_dir, phi, b_hf, b_lf, b_tot, err_hf, err_lf, a0=None, share0=None, limit=0.05, extended=False): r""" Diagnostic polar plot of Diagnostic efficiency (DE) for multiple evaluations. Note that points are used rather than arrows. Displaying multiple arrows would deteriorate visual comprehension. Parameters ---------- brel_mean : (N,)array_like relative mean bias as 1-D array temp_cor : (N,)array_like temporal correlation as 1-D array eff_de : (N,)array_like diagnostic efficiency as 1-D array b_dir : (N,)array_like direction of bias as 1-D array phi : (N,)array_like angle as 1-D array (in radians) b_hf : (N,)array_like high flow bias b_lf : (N,)array_like low flow bias b_tot : (N,)array_like absolute total relative bias err_hf : (N,)array_like contribution of high flow errors err_lf : (N,)array_like contribution of low flow errors a0 : (N,)array_like agreement of zero values share0 : float share of zero values in observations limit : float, optional Deviation of metric terms used to calculate the threshold of DE for which diagnosis is available. The default is 0.05. extended : boolean, optional If True, density plot is displayed. In addtion, the density plot is displayed besides the polar plot. The default is, that only the diagnostic polar plot is displayed. Returns ---------- fig : Figure diagnostic polar plot Notes ---------- .. math:: \varphi = arctan2(\overline{B_{rel}}, B_{slope}) Examples -------- Provide arrays with equal length >>> from de import de >>> import numpy as np >>> brel_mean = np.array([0.1, 0.15, 0.2]) >>> temp_cor = np.array([0.9, 0.85, 0.8]) >>> eff_de = np.array([0.21, 0.24, 0.35]) >>> b_dir = np.array([1, 1, 1]) >>> phi = np.array([0.58, 0.98, 0.78) >>> b_hf = np.array([0.2, 0.15, 0.2]) >>> b_lf = np.array([0.2, 0.05, 0.3]) >>> b_tot = np.array([0.4, 0.2, 0.5]) >>> err_hf = np.array([0.5, 0.75, 0.4]) >>> err_lf = np.array([0.5, 0.25, 0.6]) >>> de.diag_polar_plot_multi(brel_mean, temp_cor, eff_de, b_dir, phi, b_hf, b_lf, b_tot, err_hf, err_lf) """ eff_max = np.max(eff_de) ll_brel_mean = brel_mean.tolist() ll_temp_cor = temp_cor.tolist() ll_eff = eff_de.tolist() ll_b_dir = b_dir.tolist() ll_phi = phi.tolist() ll_b_hf = b_hf.tolist() ll_b_lf = b_lf.tolist() ll_b_tot = b_tot.tolist() ll_err_hf = err_hf.tolist() ll_err_lf = err_lf.tolist() # convert temporal correlation to color norm = matplotlib.colors.Normalize(vmin=0, vmax=1.0) delta = 0.01 # for spacing # determine axis limits if eff_max <= 1: ax_lim = 1.2 yy = np.arange(0.01, ax_lim, delta) c_levels = np.arange(0, ax_lim, 0.2) elif eff_max > 1 and eff_max <= 2: ax_lim = 2.2 yy = np.arange(0.01, ax_lim, delta) c_levels = np.arange(0, ax_lim, 0.2) elif eff_max > 2 and eff_max <= 3: ax_lim = 3.2 yy = np.arange(0.01, ax_lim, delta) c_levels = np.arange(0, ax_lim, 0.2) elif eff_max > 3: raise ValueError("Some values of 'DE' are too large for visualization!", eff_max) len_yy = len(yy) # arrays to plot contour lines of DE xx = np.radians(np.linspace(0, 360, len_yy)) theta, r = np.meshgrid(xx, yy) # diagnostic polar plot if not extended: fig, ax = plt.subplots( figsize=(3, 3), subplot_kw=dict(projection="polar"), constrained_layout=True ) # dummie plot for colorbar of temporal correlation cs = np.arange(0, 1.1, 0.1) dummie_cax = ax.scatter(cs, cs, c=cs, cmap="plasma_r") # Clear axis ax.cla() # plot regions ax.plot( (0, np.deg2rad(45)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(135)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(225)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(315)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) # contours of DE cp = ax.contour(theta, r, r, colors="darkgray", levels=c_levels, zorder=1, linewidths=1) cl = ax.clabel(cp, inline=True, fmt="%1.1f", colors="dimgrey") # threshold efficiency for FBM eff_l = np.sqrt((limit) ** 2 + (limit) ** 2 + (limit) ** 2) # loop over each data point zz = zip(ll_brel_mean, ll_temp_cor, ll_eff, ll_b_dir, ll_phi, ll_b_hf, ll_b_lf, ll_b_tot, ll_err_hf, ll_err_lf) for (bm, r, eff, bd, ang, bhf, blf, btot, errhf, errlf) in zz: # convert temporal correlation to color cmap = cm.get_cmap('plasma_r') rgba_color = cmap(norm(r)) # relation of high flow errors and low flow errors which explain # the total FDC error if btot > 0: exp_err = (abs(bhf) + abs(blf)) / btot elif btot == 0: exp_err = 0 # calculate pies to display error contribution of high flows and low # flows # calculate the points of the first pie marker # these are just the origin (0, 0) + some (cos, sin) points on a circle r1 = abs(errhf)/2 x1 = np.cos(2 * np.pi * np.linspace(0.25 - r1/2, 0.25 + r1/2)) y1 = np.sin(2 * np.pi * np.linspace(0.25 - r1/2, 0.25 + r1/2)) xy1 = np.row_stack([[0, 0], np.column_stack([x1, y1])]) s1 = np.abs(xy1).max() r2 = abs(errlf)/2 x2 = np.cos(2 * np.pi * np.linspace(0.75 - r2/2, 0.75 + r2/2)) y2 = np.sin(2 * np.pi * np.linspace(0.75 - r2/2, 0.75 + r2/2)) xy2 = np.row_stack([[0, 0], np.column_stack([x2, y2])]) s2 = np.abs(xy2).max() # diagnose the error if abs(bm) <= limit and exp_err > limit and eff > eff_l: c0 = ax.scatter(ang, eff, marker=xy1, s=s1 * 50, facecolor=rgba_color, zorder=3) c1 = ax.scatter(ang, eff, marker=xy2, s=s2 * 50, facecolor=rgba_color, zorder=3) c = ax.scatter(ang, eff, s=4, facecolor=rgba_color, zorder=3) c2 = ax.scatter(ang, eff, s=1, facecolor='grey', zorder=3) elif abs(bm) > limit and exp_err <= limit and eff > eff_l: c0 = ax.scatter(ang, eff, marker=xy1, s=s1 * 50, facecolor=rgba_color, zorder=3) c1 = ax.scatter(ang, eff, marker=xy2, s=s2 * 50, facecolor=rgba_color, zorder=3) c = ax.scatter(ang, eff, s=4, facecolor=rgba_color, zorder=3) c2 = ax.scatter(ang, eff, s=1, facecolor='grey', zorder=3) elif abs(bm) > limit and exp_err > limit and eff > eff_l: c0 = ax.scatter(ang, eff, marker=xy1, s=s1 * 50, facecolor=rgba_color, zorder=3) c1 = ax.scatter(ang, eff, marker=xy2, s=s2 * 50, facecolor=rgba_color, zorder=3) c = ax.scatter(ang, eff, s=4, facecolor=rgba_color, zorder=3) c2 = ax.scatter(ang, eff, s=1, facecolor='grey', zorder=3) # FBM elif abs(bm) <= limit and exp_err <= limit and eff > eff_l: ax.annotate( "", xytext=(0, 0), xy=(0, eff), arrowprops=dict(facecolor=rgba_color), zorder=2) ax.annotate( "", xytext=(0, 0), xy=(np.pi, eff), arrowprops=dict(facecolor=rgba_color), zorder=2 ) # FGM elif abs(bm) <= limit and exp_err <= limit and eff <= eff_l: c = ax.scatter(ang, eff, color=rgba_color, zorder=3) c2 = ax.scatter(ang, eff, s=1, facecolor='grey', zorder=3) # legend for error contribution of high flows and low flows rl1 = 1/2 xl1 = np.cos(2 * np.pi * np.linspace(0.25 - rl1/2, 0.25 + rl1/2)) yl1 = np.sin(2 * np.pi * np.linspace(0.25 - rl1/2, 0.25 + rl1/2)) xyl1 = np.row_stack([[0, 0], np.column_stack([xl1, yl1])]) sl1 = np.abs(xyl1).max() rl2 = 1/2 xl2 = np.cos(2 * np.pi * np.linspace(0.75 - rl2/2, 0.75 + rl2/2)) yl2 = np.sin(2 * np.pi * np.linspace(0.75 - rl2/2, 0.75 + rl2/2)) xyl2 = np.row_stack([[0, 0], np.column_stack([xl2, yl2])]) sl2 = np.abs(xyl2).max() ax.scatter([], [], color='k', zorder=2, marker=xyl1, s=sl1 * 50, label=r'$\epsilon_{hf}=1$') ax.scatter([], [], color='k', zorder=2, marker=xyl2, s=sl2 * 50, label=r'$\epsilon_{lf}=1$') ax.legend(loc='upper right', fancybox=False, frameon=False, bbox_to_anchor=(1.2, 1.12), handletextpad=0.2) ax.set_rticks([]) # turn default ticks off ax.set_rmin(0) if eff_max <= 1: ax.set_rmax(1) elif eff_max > 1: ax.set_rmax(ax_lim) # turn labels and grid off ax.tick_params( labelleft=False, labelright=False, labeltop=False, labelbottom=True, grid_alpha=0.01, ) ticks_loc = ax.get_xticks().tolist() ax.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) ax.set_xticklabels(["", "", "", "", "", "", "", ""]) ax.text( -0.17, 0.5, "High value overestimation -", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( -0.11, 0.5, "Low value underestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( -0.04, 0.5, r"$B_{slope}$ < 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.18, 0.5, "High value underestimation -", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.12, 0.5, "Low value overestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.06, 0.5, r"$B_{slope}$ > 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.12, "Constant negative offset", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.05, r"$\overline{B_{rel}}$ < 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.12, "Constant positive offset", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.05, r"$\overline{B_{rel}}$ > 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) # add colorbar for temporal correlation cbar = fig.colorbar( dummie_cax, ax=ax, orientation="horizontal", ticks=[1, 0.5, 0], shrink=0.8 ) cbar.set_label("r [-]\nTiming error", labelpad=4) cbar.set_ticklabels(["1", "0.5", "<0"]) cbar.ax.tick_params(direction="in") if a0 is not None and share0 is not None: a0_avg = np.round(np.mean(a0), 2) a0_std = np.round(np.std(a0), 2) ax.text( 0.8, 0.03, f"$s_0$ [-]: {share0}\n$a_0$ [-]: {a0_avg} ± {a0_std}", va="center", ha="left", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) elif a0 is not None and share0 is None: a0_avg = np.round(np.mean(a0), 2) a0_std = np.round(np.std(a0), 2) ax.text( 0.8, 0.03, f"$a_0$ [-]:\n {a0_avg} ± {a0_std}", va="center", ha="left", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) return fig elif extended: fig = plt.figure(figsize=(6, 3), constrained_layout=True) gs = fig.add_gridspec(1, 2) ax = fig.add_subplot(gs[0, 0], projection="polar") # add_axes([xmin,ymin,dx,dy]) ax1 = fig.add_axes([0.6, 0.15, 0.32, 0.32], frameon=True) ax2 = fig.add_axes([0.6, 0.6, 0.32, 0.32], frameon=True) # dummie plot for colorbar of temporal correlation cs = np.arange(0, 1.1, 0.1) dummie_cax = ax.scatter(cs, cs, c=cs, cmap="plasma_r") # Clear axis ax.cla() # plot regions ax.plot( (0, np.deg2rad(45)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(135)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(225)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(315)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) # contours of DE cp = ax.contour(theta, r, r, colors="darkgray", levels=c_levels, zorder=1, linewidths=1) cl = ax.clabel(cp, inline=True, fmt="%1.1f", colors="dimgrey") # threshold efficiency for FBM eff_l = np.sqrt((limit) ** 2 + (limit) ** 2 + (limit) ** 2) # loop over each data point zz = zip(ll_brel_mean, ll_temp_cor, ll_eff, ll_b_dir, ll_phi, ll_b_hf, ll_b_lf, ll_b_tot, ll_err_hf, ll_err_lf) for (bm, r, eff, bd, ang, bhf, blf, btot, errhf, errlf) in zz: # convert temporal correlation to color cmap = cm.get_cmap('plasma_r') rgba_color = cmap(norm(r)) # relation of high flow errors and low flow errors which explain # the total FDC error if btot > 0: exp_err = (abs(bhf) + abs(blf)) / btot elif btot == 0: exp_err = 0 # calculate pies to display error contribution of high flows and low # flows # calculate the points of the first pie marker # these are just the origin (0, 0) + some (cos, sin) points on a circle r1 = abs(errhf)/2 x1 = np.cos(2 * np.pi * np.linspace(0.25 - r1/2, 0.25 + r1/2)) y1 = np.sin(2 * np.pi * np.linspace(0.25 - r1/2, 0.25 + r1/2)) xy1 = np.row_stack([[0, 0], np.column_stack([x1, y1])]) s1 = np.abs(xy1).max() r2 = abs(errlf)/2 x2 = np.cos(2 * np.pi * np.linspace(0.75 - r2/2, 0.75 + r2/2)) y2 = np.sin(2 * np.pi * np.linspace(0.75 - r2/2, 0.75 + r2/2)) xy2 = np.row_stack([[0, 0], np.column_stack([x2, y2])]) s2 = np.abs(xy2).max() # diagnose the error if abs(bm) <= limit and exp_err > limit and eff > eff_l: c0 = ax.scatter(ang, eff, marker=xy1, s=s1 * 50, facecolor=rgba_color, zorder=3) c1 = ax.scatter(ang, eff, marker=xy2, s=s2 * 50, facecolor=rgba_color, zorder=3) c = ax.scatter(ang, eff, s=4, facecolor=rgba_color, zorder=3) c2 = ax.scatter(ang, eff, s=1, facecolor='grey', zorder=3) elif abs(bm) > limit and exp_err <= limit and eff > eff_l: c0 = ax.scatter(ang, eff, marker=xy1, s=s1 * 50, facecolor=rgba_color, zorder=3) c1 = ax.scatter(ang, eff, marker=xy2, s=s2 * 50, facecolor=rgba_color, zorder=3) c = ax.scatter(ang, eff, s=4, facecolor=rgba_color, zorder=3) c2 = ax.scatter(ang, eff, s=1, facecolor='grey', zorder=3) elif abs(bm) > limit and exp_err > limit and eff > eff_l: c0 = ax.scatter(ang, eff, marker=xy1, s=s1 * 50, facecolor=rgba_color, zorder=3) c1 = ax.scatter(ang, eff, marker=xy2, s=s2 * 50, facecolor=rgba_color, zorder=3) c = ax.scatter(ang, eff, s=4, facecolor=rgba_color, zorder=3) c2 = ax.scatter(ang, eff, s=1, facecolor='grey', zorder=3) # FBM elif abs(bm) <= limit and exp_err <= limit and eff > eff_l: ax.annotate( "", xytext=(0, 0), xy=(0, eff), arrowprops=dict(facecolor=rgba_color), zorder=2) ax.annotate( "", xytext=(0, 0), xy=(np.pi, eff), arrowprops=dict(facecolor=rgba_color), zorder=2 ) # FGM elif abs(bm) <= limit and exp_err <= limit and eff <= eff_l: c = ax.scatter(ang, eff, color=rgba_color, zorder=3) c2 = ax.scatter(ang, eff, s=1, facecolor='grey', zorder=3) # legend for error contribution of high flows and low flows rl1 = 1/2 xl1 = np.cos(2 * np.pi * np.linspace(0.25 - rl1/2, 0.25 + rl1/2)) yl1 = np.sin(2 * np.pi * np.linspace(0.25 - rl1/2, 0.25 + rl1/2)) xyl1 = np.row_stack([[0, 0], np.column_stack([xl1, yl1])]) sl1 = np.abs(xyl1).max() rl2 = 1/2 xl2 = np.cos(2 * np.pi * np.linspace(0.75 - rl2/2, 0.75 + rl2/2)) yl2 = np.sin(2 * np.pi * np.linspace(0.75 - rl2/2, 0.75 + rl2/2)) xyl2 = np.row_stack([[0, 0], np.column_stack([xl2, yl2])]) sl2 = np.abs(xyl2).max() ax.scatter([], [], color='k', zorder=2, marker=xyl1, s=sl1 * 50, label=r'high values ($\epsilon_{hf}=1$)') ax.scatter([], [], color='k', zorder=2, marker=xyl2, s=sl2 * 50, label=r'low values ($\epsilon_{lf}=1$)') ax.legend(loc='upper right', title="Error contribution of", fancybox=False, frameon=False, bbox_to_anchor=(1.3, 1.1), handletextpad=0.1) ax.set_rticks([]) # turn default ticks off ax.set_rmin(0) if eff_max <= 1: ax.set_rmax(1) elif eff_max > 1: ax.set_rmax(ax_lim) # turn labels and grid off ax.tick_params( labelleft=False, labelright=False, labeltop=False, labelbottom=True, grid_alpha=0.01, ) ticks_loc = ax.get_xticks().tolist() ax.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) ax.set_xticklabels(["", "", "", "", "", r"0$^{\circ}$ (360$^{\circ}$)", "", ""]) ax.text( -0.16, 0.5, "High value overestimation -", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( -0.11, 0.5, "Low value underestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( -0.05, 0.5, r"$B_{slope}$ < 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.16, 0.5, "High value underestimation -", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.11, 0.5, "Low value overestimation", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.05, 0.5, r"$B_{slope}$ > 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.11, "Constant negative offset", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.05, r"$\overline{B_{rel}}$ < 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.11, "Constant positive offset", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.05, r"$\overline{B_{rel}}$ > 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) # add colorbar for temporal correlation cbar = fig.colorbar( dummie_cax, ax=ax, orientation="horizontal", ticks=[1, 0.5, 0], shrink=0.8 ) cbar.set_label("r [-]\nTiming error", labelpad=4) cbar.set_ticklabels(["1", "0.5", "<0"]) cbar.ax.tick_params(direction="in") # convert to degrees diag_deg = (phi * (180 / np.pi)) + 135 diag_deg[diag_deg < 0] = 360 - diag_deg[diag_deg < 0] # 1-D density plot g = sns.kdeplot(x=diag_deg, color="k", ax=ax1) ax1.set_xticks([0, 90, 180, 270, 360]) ax1.set_xlim(0, 360) ax1.set_ylim(0,) ax1.set(ylabel="Density", xlabel=r"[$^\circ$]") # error contribution of high flows and low flows errc = pd.DataFrame(index=range(len(eff_de)), columns=['hf', 'lf']) errc.loc[:, 'hf'] = err_hf errc.loc[:, 'lf'] = err_lf sns.violinplot(data=errc, ax=ax2, inner="quartile") ax2.set_ylabel(r'$\epsilon$ [-]') ax2.set_xticklabels(['high values', 'low values']) # 2-D density plot cmap = cm.get_cmap('plasma_r') r_colors = cmap(norm(temp_cor)) g = sns.jointplot( x=diag_deg, y=eff_de, kind="kde", zorder=1, n_levels=20, cmap="Greys", thresh=0.05, marginal_kws={"color": "k"}, ).plot_joint(plt.scatter, c=r_colors, alpha=0.4, zorder=2) g.set_axis_labels(r"[$^\circ$]", "DE [-]") g.ax_joint.set_xticks([0, 90, 180, 270, 360]) g.ax_joint.set_xlim(0, 360) g.ax_joint.set_ylim(0, ax_lim) g.ax_marg_x.set_xticks([0, 90, 180, 270, 360]) kde_data = g.ax_marg_x.get_lines()[0].get_data() kde_xx = kde_data[0] kde_yy = kde_data[1] kde_data = g.ax_marg_y.get_lines()[0].get_data() kde_xx = kde_data[0] kde_yy = kde_data[1] norm = matplotlib.colors.Normalize(vmin=0, vmax=ax_lim) colors = cm.Greens_r(norm(kde_yy)) npts = len(kde_xx) for i in range(npts - 1): g.ax_marg_y.fill_betweenx( [kde_yy[i], kde_yy[i + 1]], [kde_xx[i], kde_xx[i + 1]], color=colors[i] ) g.fig.tight_layout() return fig, g.fig
def gdiag_polar_plot(eff, comp1, comp2, comp3, limit=0.05): # pragma: no cover r""" Generic diagnostic polar plot for single evaluation. Parameters ---------- eff : float efficiency measure comp1 : float metric component 1 comp2 : float metric component 2 comp3 : float metric component 3 limit : float, optional Deviation of metric terms used to calculate the threshold of DE for which diagnosis is available. The default is 0.05. Returns ---------- fig : Figure diagnostic polar plot Notes ---------- .. math:: \varphi = arctan2(comp1, comp2) """ # convert to radians # (y, x) Trigonometric inverse tangent phi = calc_phi(comp1, comp2) # convert metric component 3 to color norm = matplotlib.colors.Normalize(vmin=0, vmax=1.0) cmap = cm.get_cmap('plasma_r') rgba_color = cmap(norm(comp3)) delta = 0.01 # for spacing # determine axis limits if eff <= 1: ax_lim = 1.2 yy = np.arange(0.01, ax_lim, delta) c_levels = np.arange(0, ax_lim, 0.2) elif eff < 1 and eff <= 2: ax_lim = 2.2 yy = np.arange(0.01, ax_lim, delta) c_levels = np.arange(0, ax_lim, 0.2) elif eff < 2 and eff <= 3: ax_lim = 3.2 yy = np.arange(0.01, ax_lim, delta) c_levels = np.arange(0, ax_lim, 0.2) elif eff > 3: raise AssertionError("Value of eff is out of bounds for visualization!", eff) len_yy = len(yy) # arrays to plot contour lines of DE xx = np.radians(np.linspace(0, 360, len_yy)) theta, r = np.meshgrid(xx, yy) # diagnostic polar plot fig, ax = plt.subplots( figsize=(3, 3), subplot_kw=dict(projection="polar"), constrained_layout=True ) # dummie plot for colorbar of temporal correlation cs = np.arange(0, 1.1, 0.1) dummie_cax = ax.scatter(cs, cs, c=cs, cmap="plasma_r") # Clear axis ax.cla() # plot regions ax.plot( (0, np.deg2rad(45)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(135)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(225)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(315)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) # contours of DE cp = ax.contour(theta, r, r, colors="darkgray", levels=c_levels, zorder=1, linewidths=1) cl = ax.clabel(cp, inline=True, fmt="%1.1f", colors="dimgrey") # threshold efficiency for FBM eff_l = np.sqrt((limit) ** 2 + (limit) ** 2 + (limit) ** 2) # relation of b_dir which explains the error if abs(comp2) > 0: exp_err = comp2 elif abs(comp2) == 0: exp_err = 0 # diagnose the error if abs(comp1) <= limit and exp_err > limit and eff > eff_l: ax.annotate( "", xytext=(0, 0), xy=(phi, eff), arrowprops=dict(facecolor=rgba_color) ) elif abs(comp1) > limit and exp_err <= limit and eff > eff_l: ax.annotate( "", xytext=(0, 0), xy=(phi, eff), arrowprops=dict(facecolor=rgba_color) ) elif abs(comp1) > limit and exp_err > limit and eff > eff_l: ax.annotate( "", xytext=(0, 0), xy=(phi, eff), arrowprops=dict(facecolor=rgba_color) ) # FBM elif abs(comp1) <= limit and exp_err <= limit and eff > eff_l: ax.annotate( "", xytext=(0, 0), xy=(0, eff), arrowprops=dict(facecolor=rgba_color) ) ax.annotate( "", xytext=(0, 0), xy=(np.pi, eff), arrowprops=dict(facecolor=rgba_color) ) # FGM elif abs(comp1) <= limit and exp_err <= limit and eff <= eff_l: c = ax.scatter(phi, eff, color=rgba_color) ax.set_rticks([]) # turn default ticks off ax.set_rmin(0) if eff <= 1: ax.set_rmax(1) elif eff > 1: ax.set_rmax(ax_lim) # turn labels and grid off ax.tick_params( labelleft=False, labelright=False, labeltop=False, labelbottom=True, grid_alpha=0.01, ) ticks_loc = ax.get_xticks().tolist() ax.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) ax.set_xticklabels(["", "", "", "", "", "", "", ""]) ax.text( -0.04, 0.5, r"Comp2 < 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.04, 0.5, r"Comp2 > 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.04, r"Comp1 < 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.04, r"Comp1 > 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) # add colorbar for temporal correlation cbar = fig.colorbar( dummie_cax, ax=ax, orientation="horizontal", ticks=[1, 0.5, 0], shrink=0.8 ) cbar.set_label("Comp3", labelpad=4) cbar.set_ticklabels(["1", "0.5", "<0"]) cbar.ax.tick_params(direction="in") return fig def gdiag_polar_plot_multi( eff, comp1, comp2, comp3, limit=0.05, extended=True ): # pragma: no cover r""" Generic diagnostic polar plot for multiple evaluations. Parameters ---------- eff : (N,)array_like efficiency measure comp1 : (N,)array_like metric component 1 comp2 : (N,)array_like metric component 2 comp3 : (N,)array_like metric component 3 limit : float, optional Deviation of metric terms used to calculate the threshold of DE for which diagnosis is available. The default is 0.05. extended : boolean, optional If True, density plot is displayed. In addtion, the density plot is displayed besides the polar plot. The default is, that only the diagnostic polar plot is displayed. Returns ---------- fig : Figure diagnostic polar plot Notes ---------- .. math:: \varphi = arctan2(comp1, comp2) """ eff_max = np.max(eff) ll_comp1 = comp1.tolist() ll_comp2 = comp2.tolist() ll_comp3 = comp3.tolist() ll_eff = eff.tolist() # convert temporal correlation to color norm = matplotlib.colors.Normalize(vmin=0, vmax=1.0) delta = 0.01 # for spacing # determine axis limits if eff_max <= 1: ax_lim = 1.2 yy = np.arange(0.01, ax_lim, delta) c_levels = np.arange(0, ax_lim, 0.2) elif eff_max > 1 and eff_max <= 2: ax_lim = 2.2 yy = np.arange(0.01, ax_lim, delta) c_levels = np.arange(0, ax_lim, 0.2) elif eff_max > 2 and eff_max <= 3: ax_lim = 3.2 yy = np.arange(0.01, ax_lim, delta) c_levels = np.arange(0, ax_lim, 0.2) elif eff_max > 3: raise ValueError( "Some values of eff are out of bounds for visualization!", eff_max ) len_yy = len(yy) # arrays to plot contour lines of DE xx = np.radians(np.linspace(0, 360, len_yy)) theta, r = np.meshgrid(xx, yy) # diagnostic polar plot if not extended: fig, ax = plt.subplots( figsize=(3, 3), subplot_kw=dict(projection="polar"), constrained_layout=True ) # dummie plot for colorbar of temporal correlation cs = np.arange(0, 1.1, 0.1) dummie_cax = ax.scatter(cs, cs, c=cs, cmap="plasma_r") # Clear axis ax.cla() # plot regions ax.plot( (0, np.deg2rad(45)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(135)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(225)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(315)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) # contours of DE cp = ax.contour(theta, r, r, colors="darkgray", levels=c_levels, zorder=1, linewidths=1) cl = ax.clabel(cp, inline=True, fmt="%1.1f", colors="dimgrey") # threshold efficiency for FBM eff_l = np.sqrt((limit) ** 2 + (limit) ** 2 + (limit) ** 2) # loop over each data point for (c1, c2, c3, eff) in zip(ll_comp1, ll_comp2, ll_comp3, ll_eff): ang = np.arctan2(c1, c2) # convert temporal correlation to color cmap = cm.get_cmap('plasma_r') rgba_color = cmap(norm(c3)) # relation of b_dir which explains the error if abs(c2) > 0: exp_err = c2 elif abs(c2) == 0: exp_err = 0 # diagnose the error if abs(c1) <= limit and exp_err > limit and eff > eff_l: c = ax.scatter(ang, eff, color=rgba_color, zorder=2) elif abs(c1) > limit and exp_err <= limit and eff > eff_l: c = ax.scatter(ang, eff, color=rgba_color, zorder=2) elif abs(c1) > limit and exp_err > limit and eff > eff_l: c = ax.scatter(ang, eff, color=rgba_color, zorder=2) # FBM elif abs(c1) <= limit and exp_err <= limit and eff > eff_l: ax.annotate( "", xytext=(0, 0), xy=(0, eff), arrowprops=dict(facecolor=rgba_color), ) ax.annotate( "", xytext=(0, 0), xy=(np.pi, eff), arrowprops=dict(facecolor=rgba_color), ) # FGM elif abs(c1) <= limit and exp_err <= limit and eff <= eff_l: c = ax.scatter(ang, eff, color=rgba_color, zorder=2) ax.set_rticks([]) # turn default ticks off ax.set_rmin(0) if eff_max <= 1: ax.set_rmax(1) elif eff_max > 1: ax.set_rmax(ax_lim) # turn labels and grid off ax.tick_params( labelleft=False, labelright=False, labeltop=False, labelbottom=True, grid_alpha=0.01, ) ticks_loc = ax.get_xticks().tolist() ax.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) ax.set_xticklabels(["", "", "", "", "", "", "", ""]) ax.text( -0.04, 0.5, r"Comp2 < 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.04, 0.5, r"Comp2 > 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.04, r"Comp1 < 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.04, r"Comp1 > 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) # add colorbar for temporal correlation cbar = fig.colorbar( dummie_cax, ax=ax, orientation="horizontal", ticks=[1, 0.5, 0], shrink=0.8 ) cbar.set_label("Comp3", labelpad=4) cbar.set_ticklabels(["1", "0.5", "<0"]) cbar.ax.tick_params(direction="in") return fig elif extended: fig = plt.figure(figsize=(6, 3), constrained_layout=True) gs = fig.add_gridspec(1, 2) ax = fig.add_subplot(gs[0, 0], projection="polar") ax1 = fig.add_axes([0.66, 0.3, 0.32, 0.32], frameon=True) # dummie plot for colorbar of temporal correlation cs = np.arange(0, 1.1, 0.1) dummie_cax = ax.scatter(cs, cs, c=cs, cmap="plasma_r") # Clear axis ax.cla() # plot regions ax.plot( (0, np.deg2rad(45)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(135)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(225)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) ax.plot( (0, np.deg2rad(315)), (0, np.max(yy)), color="lightgray", linewidth=1, ls="--", zorder=0, ) # contours of DE cp = ax.contour(theta, r, r, colors="darkgray", levels=c_levels, zorder=1, linewidths=1) cl = ax.clabel(cp, inline=True, fmt="%1.1f", colors="dimgrey") # threshold efficiency for FBM eff_l = np.sqrt((limit) ** 2 + (limit) ** 2 + (limit) ** 2) # loop over each data point for (c1, c2, c3, eff) in zip(ll_comp1, ll_comp2, ll_comp3, ll_eff): # normalize threshold with mean flow becnhmark ang = np.arctan2(c1, c2) # convert temporal correlation to color cmap = cm.get_cmap('plasma_r') rgba_color = cmap(norm(c3)) # relation of b_dir which explains the error if abs(c2) > 0: exp_err = c2 elif abs(c2) == 0: exp_err = 0 # diagnose the error if abs(c1) <= limit and exp_err > limit and eff > eff_l: c = ax.scatter(ang, eff, color=rgba_color, zorder=2) elif abs(c1) > limit and exp_err <= limit and eff > eff_l: c = ax.scatter(ang, eff, color=rgba_color, zorder=2) elif abs(c1) > limit and exp_err > limit and eff > eff_l: c = ax.scatter(ang, eff, color=rgba_color, zorder=2) # FBM elif abs(c1) <= limit and exp_err <= limit and eff > eff_l: ax.annotate( "", xytext=(0, 0), xy=(0, eff), arrowprops=dict(facecolor=rgba_color), ) ax.annotate( "", xytext=(0, 0), xy=(np.pi, eff), arrowprops=dict(facecolor=rgba_color), ) # FGM elif abs(c1) <= limit and exp_err <= limit and eff <= eff_l: c = ax.scatter(ang, eff, color=rgba_color, zorder=2) ax.set_rticks([]) # turn default ticks off ax.set_rmin(0) if eff_max <= 1: ax.set_rmax(1) elif eff_max > 1: ax.set_rmax(ax_lim) # turn labels and grid off ax.tick_params( labelleft=False, labelright=False, labeltop=False, labelbottom=True, grid_alpha=0.01, ) ticks_loc = ax.get_xticks().tolist() ax.xaxis.set_major_locator(mticker.FixedLocator(ticks_loc)) ax.set_xticklabels(["", "", "", "", "", "", "", ""]) ax.text( -0.04, 0.5, r"Comp2 < 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 1.04, 0.5, r"Comp2 > 0", va="center", ha="center", rotation=90, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, -0.04, r"Comp1 < 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) ax.text( 0.5, 1.04, r"Comp1 > 0", va="center", ha="center", rotation=0, rotation_mode="anchor", transform=ax.transAxes, ) # add colorbar for temporal correlation cbar = fig.colorbar( dummie_cax, ax=ax, orientation="horizontal", ticks=[1, 0.5, 0], shrink=0.8 ) cbar.set_label("Comp3", labelpad=4) cbar.set_ticklabels(["1", "0.5", "<0"]) cbar.ax.tick_params(direction="in") # convert to degrees phi = np.arctan2(comp1, comp2) diag_deg = (phi * (180 / np.pi)) + 135 diag_deg[diag_deg < 0] = 360 - diag_deg[diag_deg < 0] # 1-D density plot g = sns.kdeplot(x=diag_deg, color="k", ax=ax1) ax1.set_xticks([0, 90, 180, 270, 360]) ax1.set_xlim(0, 360) ax1.set_ylim(0,) ax1.set(ylabel="Density", xlabel=r"[$^\circ$]") # 2-D density plot cmap = cm.get_cmap('plasma_r') c3_colors = cmap(norm(comp3)) g = sns.jointplot( x=diag_deg, y=eff, kind="kde", zorder=1, n_levels=20, cmap="Greys", thresh=0.05, marginal_kws={"color": "k"}, ).plot_joint(plt.scatter, c=c3_colors, alpha=0.4, zorder=2) g.set_axis_labels(r"[$^\circ$]", r"Eff [-]") g.ax_joint.set_xticks([0, 90, 180, 270, 360]) g.ax_joint.set_xlim(0, 360) g.ax_joint.set_ylim(0, ax_lim) g.ax_marg_x.set_xticks([0, 90, 180, 270, 360]) kde_data = g.ax_marg_y.get_lines()[0].get_data() kde_xx = kde_data[0] kde_yy = kde_data[1] norm = matplotlib.colors.Normalize(vmin=0, vmax=ax_lim) colors = cm.Greens_r(norm(kde_yy)) npts = len(kde_xx) for i in range(npts - 1): g.ax_marg_y.fill_betweenx( [kde_yy[i], kde_yy[i + 1]], [kde_xx[i], kde_xx[i + 1]], color=colors[i] ) g.fig.tight_layout() return fig, g.fig