# HDP — Heatwave Diagnostics Package > HDP is an open-source Python package for computing **heatwave metrics** from daily, > gridded climate-model output (e.g. CMIP6 / large ensembles). Its distinguishing feature is > efficiently sampling a **parameter space** of heat *measures* × percentile *thresholds* × > heatwave *definitions* in a single pass, built on xarray + Dask + Numba so it scales from a > laptop to an HPC cluster. It also generates standardized summary-figure decks as Jupyter > notebooks. This file is a self-contained context document for LLMs assisting users with the HDP. Everything needed to write correct workflows is inline below; deeper references are linked at the end. - **Package (PyPI):** `hdp-python` · **Import name:** `hdp` · **Version:** 1.0.2 · **Python:** ≥ 3.12.3 - **License:** MIT · **Repo:** https://github.com/AgentOxygen/HDP · **Docs:** https://hdp.readthedocs.io - **Paper (cite this):** Cummins, C., & Persad, G. (2026). *Heatwave Diagnostics Package.* JOSS, 11(118), 8111. https://doi.org/10.21105/joss.08111 --- ## Mental model: the 3-step workflow HDP always follows the same pipeline. Keep these terms straight: - **Measure** — a daily gridded heat variable (e.g. daily max/mean temperature `tas`/`tasmax`, or a derived heat index). You provide two measures: a **baseline** and a **test**. - **Threshold** — a percentile of the *baseline* measure, computed per day-of-year (seasonally varying). Defines "how hot is hot." - **Definition** — an integer triple describing what temporal pattern of hot days counts as a heatwave event. - **Parameter space** — the full grid of (measures × thresholds/percentiles × definitions) that HDP evaluates at once. ``` 1. format measures → hdp.measure.format_standard_measures([...]) # baseline + test 2. compute thresholds → hdp.threshold.compute_thresholds(baseline, ...) # percentiles of baseline 3. compute metrics → hdp.metric.compute_group_metrics(test, thr, defs)# test vs threshold (4. summary figures) → create_notebook(metrics).save_notebook("out.ipynb") ``` The output of step 3 is a standard `xarray.Dataset` of the four heatwave metrics (HWF, HWN, HWD, HWA) over the parameter space — analyze or save it like any xarray object. --- ## Install & import ```bash pip install hdp-python ``` ```python import hdp.utils # mock-data generators import hdp.measure # format/convert input measures, heat index import hdp.threshold # percentile thresholds import hdp.metric # heatwave metrics from hdp.graphics.notebook import create_notebook # figure decks import numpy as np ``` Core dependencies (auto-installed): numpy, `xarray>=2025.1.1`, cartopy, `numba>=0.60.0`, nc_time_axis, `dask[complete]`, zarr, netCDF4, tqdm, ipywidgets, nbformat. The cloud example below additionally needs `s3fs` and `pandas` (install separately). --- ## Core API ### `hdp.measure.format_standard_measures(temp_datasets, rh=None) -> xarray.Dataset` Prepares raw temperature DataArrays for HDP. Validates/auto-converts units to °C, tags HDP metadata, and merges them into one Dataset. - `temp_datasets: list[xarray.DataArray]` — each must have `.name` and `.attrs["units"]`. - `rh: xarray.DataArray = None` — optional relative humidity; if given, also produces a heat-index measure per temperature, named `"{temp.name}_hi"`. - Pass **one** measure per call when you want a clean baseline vs test split (call it twice). ### `hdp.threshold.compute_thresholds(baseline_dataset, percentiles, no_season=False, rolling_window_size=7, fixed_value=None) -> xarray.Dataset` Computes seasonally-varying percentile thresholds for every variable in `baseline_dataset` (a formatted measure Dataset). Returns thresholds named `"{measure}_threshold"`. - `percentiles: np.ndarray` — fractions in `(0, 1)`, e.g. `np.arange(0.9, 1.0, 0.01)`. - `no_season=True` — single whole-year percentile instead of per-day-of-year windows. - `rolling_window_size=7` — day-of-year window radius source for the percentile sample. - Single-array variant: `hdp.threshold.compute_threshold(baseline_dataarray, percentiles, ...)`. - Path-based (read/write disk): `hdp.threshold.compute_threshold_io(baseline_path, baseline_var, output_path, percentiles, ..., overwrite=False)`. ### `hdp.metric.compute_group_metrics(measures, thresholds, hw_definitions, include_threshold=False, check_variables=True) -> xarray.Dataset` Computes HWN/HWF/HWD/HWA for every compatible (measure, threshold) pair across all definitions and percentiles. Measures are matched to thresholds by the `baseline_variable` attribute. - `hw_definitions: list[list[int]]` — e.g. `[[3,0,0], [3,1,1], [4,0,0], [5,1,1]]` (see semantics below). - `include_threshold=True` — also embeds the threshold arrays in the output (can be large). - Single-pair variant: `hdp.metric.compute_individual_metrics(measure, threshold, hw_definitions, ...)`. - Path-based: `hdp.metric.compute_metrics_io(output_path, measure_path, measure_var, threshold_path, hw_definitions, ..., overwrite=False)`. ### `create_notebook(metric_ds).save_notebook(path, title=None)` `from hdp.graphics.notebook import create_notebook`. Builds a standardized figure deck from a **metric** Dataset and writes it to a `.ipynb`. (Only `hdp_type == "metric"` datasets produce figures; measure/threshold inputs are currently no-ops.) ### Plotting helpers — `hdp.graphics.figure` Return matplotlib Figures you can customize: `plot_metric_timeseries(metric_da)`, `plot_metric_decadal_maps(metric_da)`, `plot_metric_parameter_comparison(metric_da)`, `plot_multi_measure_metric_comparisons(metric_ds)`. ### Mock data — `hdp.utils` For reproducible examples/tests (no downloads): `generate_test_control_dataarray(add_noise=False, grid_shape=(2,3))`, `generate_test_warming_dataarray(...)` (adds a warming trend), `generate_test_rh_dataarray(...)`. They return a `noleap`-calendar cftime DataArray named `test_temperature_data` with `units="degC"`, dims `(lon, lat, time)`, already chunked. --- ## Data conventions & gotchas (read before writing a workflow) - **Input DataArray requirements:** must have a `.name`, `.attrs["units"]` in `{degC, degK, degF, C, K, F}`, and dims `time`, `lat`, `lon`. Units are auto-converted to °C. - **Time axis must be cftime** (the code reads `.dayofyr` and `.calendar` on timestamps). Use `xarray.date_range(..., use_cftime=True)` / `xr.open_*(..., use_cftime=True)`. - **Time must be ONE contiguous chunk:** `da = da.chunk({"time": -1})`. Chunk over **space** (`lat`/`lon`) instead — the heatwave indexing runs per-timeseries and the threshold step requires the full time axis in one chunk. This is the single most common mistake. - **Percentiles** are fractions in `(0, 1)` (e.g. `0.9`), not `0–100`. - **Definition triple `[min_duration, max_break, max_subs]`** (stored as the `"a-b-c"` coord on the `definition` dimension): 1. `min_duration` — minimum consecutive hot days to start a heatwave event. 2. `max_break` — maximum non-hot ("break") days allowed after the start before the event ends. 3. `max_subs` — maximum number of subsequent sub-events joined back across breaks. Sampling many definitions in one run is HDP's core value — it avoids re-running the expensive computation for each heatwave "type." - **The four metrics** (computed per heatwave season per year): - `HWF` — Heatwave Frequency: number of heatwave **days** in the season. - `HWN` — Heatwave Number: number of distinct heatwave **events**. - `HWD` — Heatwave Duration: length (days) of the **longest** event. - `HWA` — Heatwave Average: **mean** event length (days). *(Note: in the source `units` attribute HWA/HWN are labeled "heatwave events"; HWA is actually a length in days.)* - **Seasons are hemisphere-aware and hard-coded:** Northern Hemisphere = boreal summer May 1 – Oct 1; Southern Hemisphere = austral summer Nov 1 – Apr 1. Metrics are aggregated per season-year, and leading/trailing **partial** seasons are dropped. The per-year `year` dimension is renamed to `time` in the output. - **Output variable naming:** metric variables are `"{measure}.{threshold}.{metric}"` with `.` as the delimiter, e.g. `test_temperature_data.test_temperature_data_threshold.HWF`. The Dataset attrs `variable_naming_desc` / `variable_naming_delimeter` document this. - **Ensembles:** a `member` dimension is supported; for thresholds, members are concatenated along time before computing percentiles. - **Saving:** `.to_zarr(path)` (default/faster) or `.to_netcdf(path)`. For zarr you may need `zarr_format=2` for compatibility (see cloud example). - **Scaling:** for anything beyond toy data, start a Dask cluster *before* the HDP calls (`LocalCluster` on one machine; an HPC-appropriate cluster otherwise). --- ## Full runnable example — sample data (no downloads) This is the canonical Quick-Start and is fully reproducible with bundled mock data. ```python from hdp.graphics.notebook import create_notebook import hdp.utils import hdp.measure import hdp.threshold import hdp.metric import numpy as np output_dir = "." # 1. Inputs: a control (baseline) and a warming (test) temperature DataArray sample_control_temp = hdp.utils.generate_test_control_dataarray(add_noise=True) sample_warming_temp = hdp.utils.generate_test_warming_dataarray(add_noise=True) # 2. Format measures (validates units, converts to degC, tags HDP metadata) baseline_measures = hdp.measure.format_standard_measures(temp_datasets=[sample_control_temp]) test_measures = hdp.measure.format_standard_measures(temp_datasets=[sample_warming_temp]) # 3. Thresholds from the baseline: 90th..99th percentiles percentiles = np.arange(0.9, 1.0, 0.01) thresholds_dataset = hdp.threshold.compute_thresholds(baseline_measures, percentiles) # 4. Heatwave metrics for the test measure across several definitions definitions = [[3,0,0], [3,1,1], [4,0,0], [4,1,1], [5,0,0], [5,1,1]] metrics_dataset = hdp.metric.compute_group_metrics( test_measures, thresholds_dataset, definitions, include_threshold=True ) metrics_dataset.to_netcdf(f"{output_dir}/sample_hw_metrics.nc", mode="w") # 5. Summary figure deck figure_notebook = create_notebook(metrics_dataset) figure_notebook.save_notebook(f"{output_dir}/sample_hw_summary_figures.ipynb") ``` --- ## Real-data example — CESM2 / CMIP6 from the cloud Reads CMIP6 daily `tas` for CESM2 directly from the public AWS S3 bucket, derives thresholds from a 1961–1990 historical baseline, and computes SSP3-7.0 metrics. Needs `s3fs` and `pandas`. Source: `docs/example_cmip_workflow/run_cmip_workflow.py`. ```python from dask.distributed import Client, LocalCluster import hdp.measure, hdp.threshold, hdp.metric from hdp.graphics.notebook import create_notebook import xarray as xr, numpy as np, pandas as pd, s3fs # Start a Dask cluster BEFORE the HDP calls (tune to your machine) cluster = LocalCluster(n_workers=6, memory_limit="16GB", threads_per_worker=2, processes=True) client = Client(cluster) # Locate CESM2 daily tas (member r4i1p1f1) in the Pangeo CMIP6 index idx = pd.read_csv("https://cmip6-pds.s3.amazonaws.com/pangeo-cmip6.csv") idx = idx.query("table_id=='day' & source_id=='CESM2'") idx = idx[idx["member_id"] == "r4i1p1f1"] ssp_info = idx.query("experiment_id=='ssp370' & variable_id=='tas'") base_info = idx.query("experiment_id=='historical' & variable_id=='tas'") fs = s3fs.S3FileSystem(anon=True) ssp370_tas = xr.open_zarr(fs.get_mapper(ssp_info["zstore"].iloc[0]), consolidated=True)["tas"] baseline_tas = xr.open_zarr(fs.get_mapper(base_info["zstore"].iloc[0]), consolidated=True)["tas"] # REQUIRED: one contiguous time chunk; chunk over space. Trim baseline to 30 years. ssp370_tas = ssp370_tas.chunk(dict(time=-1, lat=24, lon=18)) baseline_tas = baseline_tas.chunk(dict(time=-1, lat=24, lon=18)).sel(time=slice("1961-01-01", "1990-12-31")) baseline_measures = hdp.measure.format_standard_measures(temp_datasets=[baseline_tas]) ssp370_measures = hdp.measure.format_standard_measures(temp_datasets=[ssp370_tas]) percentiles = np.arange(0.9, 1.0, 0.01) thresholds = hdp.threshold.compute_thresholds(baseline_measures, percentiles) definitions = [[3,1,0], [3,1,1], [4,0,0], [4,1,1], [5,0,0], [5,1,1]] metrics = hdp.metric.compute_group_metrics(ssp370_measures, thresholds, definitions) metrics.to_zarr("cesm2_ssp370_hw_metrics.zarr", mode="w", compute=True, zarr_format=2) # Re-open from disk to avoid recomputation, then build the figure deck metrics_disk = xr.open_zarr("cesm2_ssp370_hw_metrics.zarr") create_notebook(metrics_disk).save_notebook("cesm2_ssp370_hw_metrics.ipynb") client.shutdown() ``` --- ## Output schema cheat-sheet - **Formatted measure** (`xarray.Dataset`): one data var per input measure; dims `time, lat, lon` (+ `member` if present); attrs include `hdp_type="measure"`, `baseline_variable`, `hdp_version`. - **Threshold** (`xarray.Dataset`): vars `"{measure}_threshold"`; dims `lat, lon, doy, percentile`; attrs include `hdp_type="threshold"`, `baseline_variable`, `baseline_start/end_time`, `baseline_calendar`, `param_percentiles`. - **Metric** (`xarray.Dataset`): vars `"{measure}.{threshold}.{HWF|HWN|HWD|HWA}"`; dims `time, lat, lon, percentile, definition` (+ `member`); `time` is the per-year season axis; attrs include `hdp_type="metric"`, `variable_naming_desc`. --- ## Further reading - Overview & quick start — https://hdp.readthedocs.io/en/latest/overview.html - Worked examples (sample + CESM2) — https://hdp.readthedocs.io/en/latest/examples.html - Full API reference — https://hdp.readthedocs.io/en/latest/api.html - Developer guide & testing — https://hdp.readthedocs.io/en/latest/dev_guide.html - Source modules (raw): [measure.py](https://raw.githubusercontent.com/AgentOxygen/HDP/refs/heads/main/hdp/measure.py) · [threshold.py](https://raw.githubusercontent.com/AgentOxygen/HDP/refs/heads/main/hdp/threshold.py) · [metric.py](https://raw.githubusercontent.com/AgentOxygen/HDP/refs/heads/main/hdp/metric.py) · [graphics/figure.py](https://raw.githubusercontent.com/AgentOxygen/HDP/refs/heads/main/hdp/graphics/figure.py) - GitHub Issues (questions/bugs) — https://github.com/AgentOxygen/HDP/issues