Skip to content
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM python:3.12-slim

WORKDIR /project

COPY . .

RUN pip install --upgrade pip
RUN pip install pytest sphinx sphinx-autobuild

RUN pip install -e .


CMD ["pytest", "-v", "hdp/tests/"]
6 changes: 0 additions & 6 deletions hdp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +0,0 @@
from hdp import hdp
from hdp import measure
from hdp import threshold
from hdp import metric
from hdp import utils
from hdp import definitions
8 changes: 5 additions & 3 deletions hdp/graphics/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,13 @@ def get_unique_metric_names(hw_ds):
return unique_metrics


def plot_map(metric_da, axis, cmap="hot"):
def plot_map(metric_da, ax, cmap="hot"):
cmap_obj = plt.get_cmap(cmap)
metric_da = metric_da.transpose(..., "lon")

cyclic_values, cyclic_lons = add_cyclic_point(metric_da.values, metric_da.lon.values, axis=-1)
ax_contour = axis.contourf(cyclic_lons, metric_da.lat.values, cyclic_values, transform=ccrs.PlateCarree(), cmap=cmap_obj)
axis.coastlines()
ax_contour = ax.contourf(cyclic_lons, metric_da.lat.values, cyclic_values, transform=ccrs.PlateCarree(), cmap=cmap_obj)
ax.coastlines()
return ax_contour


Expand Down
51 changes: 50 additions & 1 deletion hdp/graphics/notebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import nbformat
from io import BytesIO
from datetime import datetime
from hdp.utils import get_func_description
from hdp.graphics.figure import *
from tqdm.auto import tqdm


class HDPNotebook():
Expand Down Expand Up @@ -92,4 +95,50 @@ def save_notebook(self, path, title=None):
notebook_node.cells.append(cell)

with open(path, 'w') as nb_file:
nbformat.write(notebook_node, nb_file)
nbformat.write(notebook_node, nb_file)


def create_notebook(hw_ds):
assert "hdp_type" in hw_ds.attrs, "Missing 'hdp_type' attribute."

notebook = HDPNotebook()

if hw_ds.attrs["hdp_type"] == "measure":
pass
elif hw_ds.attrs["hdp_type"] == "threshold":
pass
elif hw_ds.attrs["hdp_type"] == "metric":
index = 1

section_name = f"Figures {index}"
notebook.create_section(section_name)
desc = get_func_description(plot_multi_measure_metric_comparisons)
notebook.add_markdown_cell(f"### Figure {index}.2 \n{desc}", section_name)
notebook.add_figure_cell(plot_multi_measure_metric_comparisons(hw_ds), section_name, alt_text=f"{section_name}")

index += 1
for metric in tqdm(list(hw_ds.data_vars), desc="Generating figures:"):
section_name = f"Figures {index}-{metric}"

notebook.create_section(section_name)
notebook.add_markdown_cell("Description of these figures.", section_name)

desc = get_func_description(plot_metric_parameter_comparison)
notebook.add_markdown_cell(f"### Figure {index}.1 \n{desc}", section_name)
notebook.add_figure_cell(plot_metric_parameter_comparison(hw_ds[metric]), section_name, alt_text=f"{section_name}")

desc = get_func_description(plot_metric_timeseries)
notebook.add_markdown_cell(f"### Figure {index}.2 \n{desc}", section_name)
notebook.add_figure_cell(plot_metric_timeseries(hw_ds[metric]), section_name, alt_text=f"{section_name}")

iindex = 3
for fig in plot_metric_decadal_maps(hw_ds[metric]):
desc = get_func_description(plot_metric_decadal_maps)
notebook.add_markdown_cell(f"### Figure {index}.{iindex} \n{desc}", section_name)
notebook.add_figure_cell(fig, section_name, alt_text=f"{section_name}")
iindex += 1
index += 1
else:
raise ValueError(f"Unexpected value for 'hdp_type' attribute, '{hw_ds.attrs["hdp_type"]}' is not 'measure', 'threshold', or 'metric'.")

return notebook
61 changes: 0 additions & 61 deletions hdp/hdp.py

This file was deleted.

2 changes: 1 addition & 1 deletion hdp/metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ def compute_individual_metrics(measure: xarray.DataArray, threshold: xarray.Data

start_ts = cftime.datetime(ds.year[0], 1, 1, calendar=measure.time.values[0].calendar)
end_ts = cftime.datetime(ds.year[-1], 1, 1, calendar=measure.time.values[0].calendar)
ds = ds.rename(dict(year="time")).assign_coords(dict(time=xarray.cftime_range(start_ts, end_ts, periods=ds.year.size)))
ds = ds.rename(dict(year="time")).assign_coords(dict(time=xarray.date_range(start_ts, end_ts, periods=ds.year.size, use_cftime=True)))

ds.attrs |= {
"description": f"Heatwave metric dataset generated by Heatwave Diagnostics Package (HDP v{get_version()})",
Expand Down
63 changes: 63 additions & 0 deletions hdp/sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env python
"""
hdp.py

Heatwave Diagnostics Package (HDP)

Entry point for package.

Developer: Cameron Cummins
Contact: cameron.cummins@utexas.edu
"""
from hdp.graphics.notebook import create_notebook
from os.path import isdir
import hdp.measure
import hdp.threshold
import hdp.metric
import numpy as np
import sys


def generate_sample_data(output_dir):
if not isdir(output_dir):
raise RuntimeError(f"Output directory '{output_dir}' does not exist!")

sample_control_temp = hdp.utils.generate_test_control_dataarray()
sample_warming_temp = hdp.utils.generate_test_warming_dataarray()

baseline_measures = hdp.measure.format_standard_measures(
temp_datasets=[sample_control_temp]
)
test_measures = hdp.measure.format_standard_measures(
temp_datasets=[sample_warming_temp]
)

percentiles = np.arange(0.9, 1.0, 0.01)

thresholds_dataset = hdp.threshold.compute_thresholds(
baseline_measures,
percentiles
)

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')

figure_notebook = create_notebook(metrics_dataset)
figure_notebook.save_notebook(f"{output_dir}/sample_hw_summary_figures.ipynb")

sample_control_temp = sample_control_temp.to_dataset()
sample_control_temp.attrs["description"] = "Mock control temperature dataset generated by HDP for unit testing."
sample_control_temp.to_netcdf(f"{output_dir}/sample_control_temp.nc", mode='w')

sample_warming_temp = sample_warming_temp.to_dataset()
sample_warming_temp.attrs["description"] = "Mock temperature dataset with warming trend generated by HDP for unit testing."
sample_warming_temp.to_netcdf(f"{output_dir}/sample_warming_temp.nc", mode='w')

if __name__ == "__main__":
print("Generating testing data and simulating a full data-to-figure workflow: ")
if len(sys.argv) != 2:
assert Exception("Please specifiy the path to output sample data and results to.")
generate_sample_data(sys.argv[1])
print("Done!")
91 changes: 51 additions & 40 deletions hdp/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,57 @@
import hdp.utils
import xarray
from hdp.utils import *
import numpy as np

from math import isclose
from xarray import DataArray

def test_get_time_stamp():
assert type(hdp.utils.get_time_stamp()) is str
assert type(get_time_stamp()) is str


def test_get_version():
assert type(hdp.utils.get_version()) is str


def test_synthetic_data_functions():
var = "test"
center_val = 10
amplitude_val = 1
units = "test_units"

ds = hdp.utils.generate_synthetic_dataset(name=var, units=units, center=center_val, amplitude=amplitude_val)
assert type(ds) is xarray.Dataset
assert len(ds.data_vars) == 1
assert var in ds

assert "units" in ds[var].attrs
assert ds[var].attrs["units"] == units
assert ds[var].dims == ("lat", "lon", "time")
assert ds[var].dtype == float
assert ds[var].time.values[0].calendar == "noleap"
assert ds[var].lat.size > 1
assert ds[var].lon.size > 1
assert ds[var].time.size >= 2*365

data = ds[var].compute()

assert np.isclose(data.mean(), center_val)
assert np.isclose(data.max(), center_val + amplitude_val)
assert np.isclose(data.values, data.values[0]).all()

exceed_data = hdp.utils.generate_exceedance_dataarray(ds[var], exceedance_pattern=[1, 0, 1, 0]).compute()

assert np.isclose(exceed_data.mean(), center_val + 0.5)
assert exceed_data.attrs == ds[var].attrs
assert exceed_data.dims == ds[var].dims
assert exceed_data.shape == ds[var].shape
assert exceed_data.dtype == ds[var].dtype
assert type(get_version()) is str


def test_control_dataarray():
control_da = generate_test_control_dataarray(grid_shape=(1,1), start_date="2000", end_date="2100")
assert type(control_da) is DataArray
var = control_da.name
assert control_da.attrs["units"] == "degC"
assert control_da.dims == ("lon", "lat", "time")
assert control_da.dtype == float
assert control_da.time.values[0].calendar == "noleap"
assert control_da.time.values.size >= 365
assert np.sum(np.isnan(control_da)) == 0

control_slope = np.polyfit(np.arange(control_da["time"].size), control_da.mean(dim=["lat", "lon"]).values, 1)[0]
assert np.abs(control_slope) < 0.01


def test_warming_dataarray():
warming_da = generate_test_warming_dataarray(grid_shape=(1,1), start_date="2000", end_date="2100")
avg_da = warming_da.mean(dim=["lat", "lon"]).values
warm_slope = np.polyfit(np.arange(avg_da.size), avg_da, 1)[0]

assert warm_slope > 0
assert not isclose(warm_slope, 0)


def test_rh_dataarray():
rh_da = generate_test_rh_dataarray()

assert rh_da.max() <= 1
assert rh_da.min() >= 0


def test_defaults_compatibility():
control_da = generate_test_control_dataarray()
warming_da = generate_test_warming_dataarray()
rh_da = generate_test_rh_dataarray()

assert control_da.shape == warming_da.shape
assert warming_da.shape == rh_da.shape

assert control_da.dims == warming_da.dims
assert warming_da.dims == rh_da.dims

assert control_da.name == warming_da.name
assert control_da.units == warming_da.units
Loading