Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
453d760
plot more percentile in diurnal cycle
leuty May 28, 2026
abdb136
plot maps of diurnal cycle
leuty May 28, 2026
ade9bb2
WIP: QQ variant
leuty May 28, 2026
18a2224
Merge remote-tracking branch 'origin/more_analysis' into more_more_an…
leuty May 28, 2026
8c96bbc
Merge remote-tracking branch 'origin/corr-diff' into more_more_analysis
leuty May 28, 2026
0280920
split temp into eval_temp.sh
leuty May 28, 2026
0940f9a
average at the end
leuty May 28, 2026
ec8df12
submit more jobs in parallel
leuty Jun 2, 2026
6476df5
manicure
leuty Jun 2, 2026
6f72447
reduce memory
leuty Jun 3, 2026
61508d9
speedup
leuty Jun 3, 2026
c608415
cleanups
leuty Jun 3, 2026
bff9615
fix issues
leuty Jun 3, 2026
a14adf8
fix
leuty Jun 3, 2026
e4cc942
add Q-bias plot for temperature
leuty Jun 3, 2026
be60d22
add the same plot for windspeed
leuty Jun 3, 2026
56987f5
cleanup
leuty Jun 3, 2026
075103d
morecleanup
leuty Jun 3, 2026
32162ae
fix config
leuty Jun 3, 2026
4a0d79e
extend x-axis
leuty Jun 3, 2026
3bb134f
fix spread
leuty Jun 4, 2026
464ac34
add smoothed version for precip and FBI
leuty Jun 4, 2026
ac2d64a
update bins
leuty Jun 4, 2026
5add4f2
change formulation for the FBI
leuty Jun 4, 2026
a9e7b0f
add smoothed
leuty Jun 4, 2026
3e6436c
smoothed wind plot
leuty Jun 9, 2026
3930d69
change x-axis
leuty Jun 9, 2026
ae7c300
reduce memory
leuty Jun 9, 2026
e12c4f7
plot wethours for more tresholds
leuty Jun 9, 2026
ed915d8
use logscale
leuty Jun 9, 2026
df5b880
plot more wethour tresholds
leuty Jun 10, 2026
d42ac92
exclude input from smoothing as it is lower res
leuty Jun 10, 2026
75c0165
change directory name
leuty Jun 10, 2026
5fad8b3
plot temp maps
leuty Jun 10, 2026
083d15a
fix temp conversion
leuty Jun 11, 2026
fb78d41
refine temperature plot
leuty Jun 15, 2026
dc63a55
more colors
leuty Jun 16, 2026
1fcf563
also compute hot days
leuty Jun 16, 2026
92ab9e6
change spacings of bins
leuty Jun 16, 2026
888d686
also show 100 mm/h
leuty Jun 16, 2026
993a3d4
fix label
leuty Jun 16, 2026
daf3d9c
add tick on the left
leuty Jun 16, 2026
ce2f253
use bin centres
leuty Jun 16, 2026
da16bc6
plot difference maps
leuty Jun 17, 2026
82f70fa
add more levels
leuty Jun 17, 2026
db938c4
reduce range
leuty Jun 17, 2026
6ffa840
add hot spell
leuty Jun 17, 2026
f30566d
makw compatible with ETCCDI
leuty Jun 17, 2026
8f93676
use 50th percentile consistently for precip
leuty Jun 17, 2026
44f090b
show less
leuty Jun 17, 2026
e0d4a5d
consolidate logit axis
leuty Jun 17, 2026
c41f646
limits
leuty Jun 17, 2026
89194e6
colorscale
leuty Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
640 changes: 640 additions & 0 deletions src/hirad/eval/bias_by_percentile_common.py

Large diffs are not rendered by default.

119 changes: 119 additions & 0 deletions src/hirad/eval/bias_by_percentile_precip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
Plots bias / MAE / spread as a function of percentile for precipitation, using a
local-then-averaged estimator (see :mod:`hirad.eval.bias_by_percentile_common`).
"""
import numpy as np

from hirad.eval.bias_by_percentile_common import (
LOGIT_PERCENTILE_TICKS_TAIL,
BiasByPercentileSpec,
apply_logit_percentile_xaxis,
finalize_percentile_plot,
new_percentile_axes,
plot_dict_curves,
run_bias_by_percentile,
)
from hirad.eval.eval_utils import make_percentile_values, parse_eval_cli


_PRECIP_SECONDARY_MMH = np.array([0.01, 0.1, 1.0, 10.0, 100.0])


def _apply_logit_xaxis(ax, frac: np.ndarray, mean_q: np.ndarray | None = None) -> None:
"""Apply logit percentile x-axis (upper tail) with a mm/h secondary axis."""
apply_logit_percentile_xaxis(
ax, frac, mean_q,
xlim_left=0.5,
percentile_ticks=LOGIT_PERCENTILE_TICKS_TAIL,
secondary_label='Mean target [mm/h]',
secondary_values=_PRECIP_SECONDARY_MMH,
)


def save_bias_by_percentile_plot(bias_data_dict, percentile_values, labels, colors,
title, xlabel, ylabel, out_path, mean_q=None) -> None:
"""Save a bias-by-percentile figure (symlog y-axis)."""
_, ax, frac = new_percentile_axes(percentile_values)
plot_dict_curves(ax, frac, bias_data_dict, labels, colors)
ax.axhline(0.0, color='black', linewidth=0.8, linestyle='--')
ax.set_yscale('symlog', linthresh=0.1, linscale=0.3)
ax.set_ylim(-10, 10)
finalize_percentile_plot(ax, frac, _apply_logit_xaxis, mean_q,
xlabel, ylabel, title, out_path)


def save_mae_by_percentile_plot(mae_data_dict, percentile_values, labels, colors,
title, xlabel, ylabel, out_path, mean_q=None) -> None:
"""Save a MAE-by-percentile figure (log y-axis)."""
_, ax, frac = new_percentile_axes(percentile_values)
plot_dict_curves(ax, frac, mae_data_dict, labels, colors, lower_clip=0)
ax.set_yscale('log')
ax.set_ylim(1e-5, 100)
finalize_percentile_plot(ax, frac, _apply_logit_xaxis, mean_q,
xlabel, ylabel, title, out_path)


def save_spread_by_percentile_plot(spread, percentile_values,
title, xlabel, ylabel, out_path, mean_q=None) -> None:
"""Save an ensemble-spread-by-percentile figure (log y-axis, no legend)."""
_, ax, frac = new_percentile_axes(percentile_values)
ax.plot(frac, spread, color='green', linewidth=2)
ax.set_yscale('log')
ax.set_ylim(1e-5, 100)
finalize_percentile_plot(ax, frac, _apply_logit_xaxis, mean_q,
xlabel, ylabel, title, out_path, legend=False)


def save_fbi_by_percentile_plot(fbi_data_dict, percentile_values, labels, colors,
title, xlabel, ylabel, out_path, mean_q=None) -> None:
"""Save a frequency-bias-index-by-percentile figure (log y-axis, ratio around 1)."""
_, ax, frac = new_percentile_axes(percentile_values)
all_vals = plot_dict_curves(ax, frac, fbi_data_dict, labels, colors, lower_clip=1e-3)
ax.axhline(1.0, color='black', linewidth=0.8, linestyle='--')
ax.set_yscale('log')
if all_vals:
ymin = float(min(np.nanmin(v) for v in all_vals)) / 1.5
ax.set_ylim(max(ymin, 1e-3), 10.0)
finalize_percentile_plot(ax, frac, _apply_logit_xaxis,
mean_q, xlabel, ylabel, title, out_path)


def _resolve_channels(indices: dict) -> tuple:
tp_out = indices['output']['tp']
tp_in = indices['input'].get('tp', tp_out)
return tp_out, tp_in


def _make_hist_bins(cfg: dict) -> np.ndarray:
n_bins = 500
return np.concatenate([np.array([0.0]), np.logspace(-2, 3.2, n_bins)])


SPEC = BiasByPercentileSpec(
var_label='precipitation',
output_prefix='precipitation',
bias_title='Precipitation Bias Over Land',
mae_title='Precipitation MAE Over Land',
spread_title='Precipitation Ensemble Spread Over Land',
fbi_title='Precipitation Frequency Bias Index Over Land',
bias_ylabel='Bias [mm/h]',
mae_ylabel='MAE [mm/h]',
spread_ylabel='Ensemble Spread [mm/h]',
fbi_ylabel='FBI (exceedance)',
percentile_values=make_percentile_values(),
resolve_channels=_resolve_channels,
make_hist_bins=_make_hist_bins,
read_scaling=lambda cfg: (cfg.get("conv_factor_hourly", 1.0), 0.0),
save_bias=save_bias_by_percentile_plot,
save_mae=save_mae_by_percentile_plot,
save_spread=save_spread_by_percentile_plot,
save_fbi=save_fbi_by_percentile_plot,
)


def main(cfg: dict) -> None:
run_bias_by_percentile(cfg, SPEC)


if __name__ == '__main__':
main(parse_eval_cli())
24 changes: 24 additions & 0 deletions src/hirad/eval/bias_by_percentile_precip_smoothed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Smoothed precipitation bias / MAE / spread by percentile plots.

Applies the same Gaussian low-pass filter to target and prediction fields before
the percentile histograms are accumulated, mitigating double-penalty effects in
the precipitation tails.
"""
from hirad.eval.bias_by_percentile_common import run_bias_by_percentile
from hirad.eval.bias_by_percentile_precip import SPEC
from hirad.eval.eval_utils import parse_eval_cli


SMOOTHING_SIGMA_KM = 20.0
GRID_RES_KM = 1.0


def main(cfg: dict) -> None:
cfg = dict(cfg)
cfg['smoothing_sigma_km'] = SMOOTHING_SIGMA_KM
cfg.setdefault('grid_res_km', GRID_RES_KM)
run_bias_by_percentile(cfg, SPEC)


if __name__ == '__main__':
main(parse_eval_cli())
106 changes: 106 additions & 0 deletions src/hirad/eval/bias_by_percentile_temp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
Plots bias / MAE / spread as a function of percentile for 2m temperature, using a
local-then-averaged estimator (see :mod:`hirad.eval.bias_by_percentile_common`).
"""
import numpy as np

from hirad.eval.bias_by_percentile_common import (
BiasByPercentileSpec,
apply_logit_percentile_xaxis,
finalize_percentile_plot,
new_percentile_axes,
plot_dict_curves,
run_bias_by_percentile,
)
from hirad.eval.eval_utils import make_percentile_values, parse_eval_cli


def _apply_logit_xaxis(ax, frac: np.ndarray, mean_q: np.ndarray | None = None) -> None:
"""Apply logit percentile x-axis with a °C secondary axis."""
apply_logit_percentile_xaxis(ax, frac, mean_q, secondary_label='Mean target [°C]')


def save_bias_by_percentile_plot(bias_data_dict, percentile_values, labels, colors,
title, xlabel, ylabel, out_path, mean_q=None) -> None:
"""Save a bias-by-percentile figure (linear y-axis, data-driven limits)."""
_, ax, frac = new_percentile_axes(percentile_values)
all_vals = plot_dict_curves(ax, frac, bias_data_dict, labels, colors)
ax.axhline(0.0, color='black', linewidth=0.8, linestyle='--')
if all_vals:
vcat = np.concatenate([np.asarray(v).ravel() for v in all_vals])
vmin, vmax = float(np.nanmin(vcat)), float(np.nanmax(vcat))
margin = max(abs(vmax - vmin) * 0.1, 0.05)
ax.set_ylim(vmin - margin, vmax + margin)
finalize_percentile_plot(ax, frac, _apply_logit_xaxis, mean_q,
xlabel, ylabel, title, out_path)


def save_mae_by_percentile_plot(mae_data_dict, percentile_values, labels, colors,
title, xlabel, ylabel, out_path, mean_q=None) -> None:
"""Save a MAE-by-percentile figure (linear y-axis, data-driven limits)."""
_, ax, frac = new_percentile_axes(percentile_values)
all_vals = plot_dict_curves(ax, frac, mae_data_dict, labels, colors, lower_clip=0)
if all_vals:
ymax = float(max(np.nanmax(v) for v in all_vals)) * 1.1
if ymax > 0:
ax.set_ylim(0, ymax)
finalize_percentile_plot(ax, frac, _apply_logit_xaxis, mean_q,
xlabel, ylabel, title, out_path)


def save_spread_by_percentile_plot(spread, percentile_values,
title, xlabel, ylabel, out_path, mean_q=None) -> None:
"""Save an ensemble-spread-by-percentile figure (linear y-axis)."""
_, ax, frac = new_percentile_axes(percentile_values)
ax.plot(frac, spread, color='green', linewidth=2, label='Ensemble spread')
ymax_spread = float(np.nanmax(spread)) * 1.1
if ymax_spread > 0:
ax.set_ylim(0, ymax_spread)
finalize_percentile_plot(ax, frac, _apply_logit_xaxis, mean_q,
xlabel, ylabel, title, out_path)


def _resolve_channels(indices: dict) -> tuple:
# Temperature channel: try '2t' first, then 't2m'
t2m_out = indices['output'].get('2t', indices['output'].get('t2m'))
t2m_in = indices['input'].get('2t', indices['input'].get('t2m', t2m_out))
if t2m_out is None:
raise ValueError("Temperature channel (2t / t2m) not found in output channels.")
return t2m_out, t2m_in


def _make_hist_bins(cfg: dict) -> np.ndarray:
# Linear histogram bins in °C — fine enough to resolve sub-degree differences
n_bins = cfg.get("n_bins", 2000)
temp_min = cfg.get("temp_bin_min_celsius", -90.0)
temp_max = cfg.get("temp_bin_max_celsius", 65.0)
return np.linspace(temp_min, temp_max, n_bins + 1)


SPEC = BiasByPercentileSpec(
var_label='2m temperature',
output_prefix='temperature',
bias_title='2m Temperature Bias Over Land',
mae_title='2m Temperature MAE Over Land',
spread_title='2m Temperature Ensemble Spread Over Land',
bias_ylabel='Bias [°C]',
mae_ylabel='MAE [°C]',
spread_ylabel='Ensemble Spread [°C]',
percentile_values=make_percentile_values(),
resolve_channels=_resolve_channels,
make_hist_bins=_make_hist_bins,
# Default: convert Kelvin → °C (conv=1.0, offset=-273.15)
read_scaling=lambda cfg: (cfg.get("temp_conv_factor", 1.0),
cfg.get("temp_offset_celsius", -273.15)),
save_bias=save_bias_by_percentile_plot,
save_mae=save_mae_by_percentile_plot,
save_spread=save_spread_by_percentile_plot,
)


def main(cfg: dict) -> None:
run_bias_by_percentile(cfg, SPEC)


if __name__ == '__main__':
main(parse_eval_cli())
130 changes: 130 additions & 0 deletions src/hirad/eval/bias_by_percentile_wind.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""
Plots bias / MAE / spread as a function of percentile for 10 m wind speed, using a
local-then-averaged estimator (see :mod:`hirad.eval.bias_by_percentile_common`).

Wind speed is derived per grid point from the two surface wind components
(``10u``, ``10v``) as ``hypot(u, v)`` before histogramming.
"""
import numpy as np

from hirad.eval.bias_by_percentile_common import (
BiasByPercentileSpec,
apply_logit_percentile_xaxis,
finalize_percentile_plot,
new_percentile_axes,
plot_dict_curves,
run_bias_by_percentile,
)
from hirad.eval.eval_utils import make_percentile_values, parse_eval_cli


def _apply_logit_xaxis(ax, frac: np.ndarray, mean_q: np.ndarray | None = None,
xlim_left: float | None = None) -> None:
"""Apply logit percentile x-axis with an m/s secondary axis."""
apply_logit_percentile_xaxis(ax, frac, mean_q, xlim_left=xlim_left,
secondary_label='Mean target [m/s]')


def save_bias_by_percentile_plot(bias_data_dict, percentile_values, labels, colors,
title, xlabel, ylabel, out_path, mean_q=None) -> None:
"""Save a bias-by-percentile figure (linear y-axis, data-driven limits)."""
_, ax, frac = new_percentile_axes(percentile_values)
all_vals = plot_dict_curves(ax, frac, bias_data_dict, labels, colors)
ax.axhline(0.0, color='black', linewidth=0.8, linestyle='--')
if all_vals:
vcat = np.concatenate([np.asarray(v).ravel() for v in all_vals])
vmin, vmax = float(np.nanmin(vcat)), float(np.nanmax(vcat))
margin = max(abs(vmax - vmin) * 0.1, 0.05)
ax.set_ylim(vmin - margin, vmax + margin)
finalize_percentile_plot(ax, frac, _apply_logit_xaxis, mean_q,
xlabel, ylabel, title, out_path)


def save_mae_by_percentile_plot(mae_data_dict, percentile_values, labels, colors,
title, xlabel, ylabel, out_path, mean_q=None) -> None:
"""Save a MAE-by-percentile figure (linear y-axis, data-driven limits)."""
_, ax, frac = new_percentile_axes(percentile_values)
all_vals = plot_dict_curves(ax, frac, mae_data_dict, labels, colors, lower_clip=0)
if all_vals:
ymax = float(max(np.nanmax(v) for v in all_vals)) * 1.1
if ymax > 0:
ax.set_ylim(0, ymax)
finalize_percentile_plot(ax, frac, _apply_logit_xaxis, mean_q,
xlabel, ylabel, title, out_path)


def save_spread_by_percentile_plot(spread, percentile_values,
title, xlabel, ylabel, out_path, mean_q=None) -> None:
"""Save an ensemble-spread-by-percentile figure (linear y-axis)."""
_, ax, frac = new_percentile_axes(percentile_values)
ax.plot(frac, spread, color='green', linewidth=2, label='Ensemble spread')
ymax_spread = float(np.nanmax(spread)) * 1.1
if ymax_spread > 0:
ax.set_ylim(0, ymax_spread)
finalize_percentile_plot(ax, frac, _apply_logit_xaxis, mean_q,
xlabel, ylabel, title, out_path)


def save_fbi_by_percentile_plot(fbi_data_dict, percentile_values, labels, colors,
title, xlabel, ylabel, out_path, mean_q=None) -> None:
"""Save a frequency-bias-index-by-percentile figure (log y-axis, ratio around 1)."""
_, ax, frac = new_percentile_axes(percentile_values)
all_vals = plot_dict_curves(ax, frac, fbi_data_dict, labels, colors, lower_clip=1e-3)
ax.axhline(1.0, color='black', linewidth=0.8, linestyle='--')
ax.set_yscale('log')
if all_vals:
ymin = float(min(np.nanmin(v) for v in all_vals)) / 1.5
ax.set_ylim(max(ymin, 1e-3), 10.0)
finalize_percentile_plot(ax, frac,
lambda ax_, frac_, mq: _apply_logit_xaxis(ax_, frac_, mq, xlim_left=0.10),
mean_q, xlabel, ylabel, title, out_path)


def _resolve_channels(indices: dict) -> tuple:
# Wind speed is derived from the two surface wind components.
u_out = indices['output'].get('10u')
v_out = indices['output'].get('10v')
if u_out is None or v_out is None:
raise ValueError("Wind components (10u / 10v) not found in output channels.")
u_in = indices['input'].get('10u', u_out)
v_in = indices['input'].get('10v', v_out)
return (u_out, v_out), (u_in, v_in)


def _make_hist_bins(cfg: dict) -> np.ndarray:
# Linear histogram bins in m/s — fine enough to resolve sub-m/s differences
n_bins = cfg.get("wind_n_bins", 1500)
speed_min = cfg.get("wind_bin_min_ms", 0.0)
speed_max = cfg.get("wind_bin_max_ms", 75.0)
return np.linspace(speed_min, speed_max, n_bins + 1)


SPEC = BiasByPercentileSpec(
var_label='10 m wind speed',
output_prefix='windspeed',
bias_title='10 m Wind Speed Bias Over Land',
mae_title='10 m Wind Speed MAE Over Land',
spread_title='10 m Wind Speed Ensemble Spread Over Land',
fbi_title='10 m Wind Speed Frequency Bias Index Over Land',
bias_ylabel='Bias [m/s]',
mae_ylabel='MAE [m/s]',
spread_ylabel='Ensemble Spread [m/s]',
fbi_ylabel='FBI (exceedance)',
percentile_values=make_percentile_values(),
resolve_channels=_resolve_channels,
make_hist_bins=_make_hist_bins,
read_scaling=lambda cfg: (cfg.get("wind_conv_factor", 1.0), 0.0),
reduce_fn=lambda flats: np.hypot(flats[0], flats[1]),
save_bias=save_bias_by_percentile_plot,
save_mae=save_mae_by_percentile_plot,
save_spread=save_spread_by_percentile_plot,
save_fbi=save_fbi_by_percentile_plot,
)


def main(cfg: dict) -> None:
run_bias_by_percentile(cfg, SPEC)


if __name__ == '__main__':
main(parse_eval_cli())
Loading