Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heat wave magnitude indicator #1926

Merged
merged 59 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
9c77c4f
Add heat wave magnitude indicator definition
seblehner Sep 16, 2024
cdd1e05
Add heat wave magnitude indicator function
seblehner Sep 16, 2024
f73936c
Add windowed_run_sum and rse function
seblehner Sep 16, 2024
d35ca73
Add wrapping functions for ufunc implementation of windowed_run_sum
seblehner Sep 16, 2024
0b55cc6
Add test for heat_wave_magnitude
seblehner Sep 16, 2024
1e0e799
Add new functions to CHANGELOG.rst
seblehner Sep 16, 2024
6c5e3e2
Rename windowed_run_sum to windowed_max_run_sum for explicity
seblehner Sep 16, 2024
394e73f
Add credentials to AUTHORS.rst and .zenodo.json
seblehner Sep 16, 2024
904c32d
Add heat_wave_magnitude to __all__
seblehner Sep 16, 2024
302fe27
Add more tests for heat_wave_magnitude
seblehner Sep 16, 2024
b49d644
Add tests for rse and windowed_max_run_sum
seblehner Sep 16, 2024
9568734
Add heat wave magnitude indicator definition
seblehner Sep 16, 2024
56693c1
Add heat wave magnitude indicator function
seblehner Sep 16, 2024
ccbb3a2
Add windowed_run_sum and rse function
seblehner Sep 16, 2024
95c57a8
Add wrapping functions for ufunc implementation of windowed_run_sum
seblehner Sep 16, 2024
51dd852
Add test for heat_wave_magnitude
seblehner Sep 16, 2024
d5ab1f6
Add new functions to CHANGELOG.rst
seblehner Sep 16, 2024
eb1628d
Rename windowed_run_sum to windowed_max_run_sum for explicity
seblehner Sep 16, 2024
a1b59bd
Add credentials to AUTHORS.rst and .zenodo.json
seblehner Sep 16, 2024
6eb6394
Add heat_wave_magnitude to __all__
seblehner Sep 16, 2024
532da97
Add more tests for heat_wave_magnitude
seblehner Sep 16, 2024
1aa9c32
Add tests for rse and windowed_max_run_sum
seblehner Sep 16, 2024
8b6d13e
Merge branch 'heat-wave-magnitude-indicator' of github.com:seblehner/…
seblehner Sep 16, 2024
9f25215
Change heat_wave_magnitude output unit handling
seblehner Sep 17, 2024
6387cb7
Fix incorrect units for heat_wave_magnitude
seblehner Sep 17, 2024
acdb62a
Change out unit handling to integral
seblehner Sep 17, 2024
64a167c
Add PR number to changelog
seblehner Sep 17, 2024
0850be1
Fix expected value for TestHeatWaveMagnitude test_simple
seblehner Sep 17, 2024
9c6be58
Add TODO entry for french translation to fr.json
seblehner Sep 17, 2024
5915b8a
Replace multi line where call with clip
seblehner Sep 17, 2024
dda8d1c
Shorten rle call input
seblehner Sep 18, 2024
4ae225e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 18, 2024
c078d7c
Fix windowed run sum for window == 1
seblehner Sep 18, 2024
b31c10d
Add tests for small window lengths
seblehner Sep 18, 2024
729aae2
Remove ufunc and 1d placeholder code
seblehner Sep 18, 2024
1cfe7b1
Add if to rle dtype conversion
seblehner Sep 18, 2024
26663a1
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 18, 2024
8c4f91b
Merge branch 'main' into heat-wave-magnitude-indicator
Zeitsperre Sep 18, 2024
e80f698
Fix error in description
seblehner Sep 18, 2024
62b24d6
Enhance indicator description
seblehner Sep 18, 2024
8d9ab44
Generalize rle and delete rse
seblehner Sep 18, 2024
2cffc3d
Merge branch 'heat-wave-magnitude-indicator' of github.com:seblehner/…
seblehner Sep 18, 2024
bec0296
Add french translation
seblehner Sep 18, 2024
232e68f
Expand indicator description
seblehner Sep 18, 2024
0a57618
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 18, 2024
f61ab29
Update xclim/indicators/atmos/_temperature.py
Zeitsperre Sep 20, 2024
35b868a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 20, 2024
7b254ce
Merge branch 'main' into heat-wave-magnitude-indicator
coxipi Sep 20, 2024
c6f98a6
heat_wave_magnitude -> hot_spell_max_magnitude
coxipi Sep 23, 2024
bcbabc2
remove abbreviation in indicator title
coxipi Sep 23, 2024
f632944
change test names
coxipi Sep 23, 2024
774b3ae
Remove rse tests
seblehner Sep 28, 2024
a148027
Merge branch 'main' into heat-wave-magnitude-indicator
Zeitsperre Sep 28, 2024
82254d8
Doc to better explain what rle does
aulemahal Oct 1, 2024
11c733e
Merge branch 'main' into heat-wave-magnitude-indicator
aulemahal Oct 2, 2024
b72ef37
add references
coxipi Oct 3, 2024
aa6f74c
remove ufunc_1dim option
coxipi Oct 3, 2024
e28db54
Merge branch 'main' into heat-wave-magnitude-indicator
coxipi Oct 3, 2024
bfa2576
Merge branch 'main' into heat-wave-magnitude-indicator
coxipi Oct 5, 2024
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
5 changes: 5 additions & 0 deletions .zenodo.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@
"name": "Wang, Hui-Min",
"affiliation": "National University of Singapore, Singapore, Singapore",
"orcid": "0000-0002-5878-7542"
},
{
"name": "Lehner, Sebastian",
"affiliation": "GeoSphere Austria, Vienna, Austria",
"orcid": "0000-0002-7562-8172"
}
],
"keywords": [
Expand Down
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ Contributors
* Javier Diez-Sierra <javier.diez@unican.es> `@JavierDiezSierra <https://github.com/JavierDiezSierra>`_
* Hui-Min Wang `@Hem-W <https://github.com/Hem-W>`_
* Adrien Lamarche `@LamAdr <https://github.com/LamAdr>`_
* Sebastian Lehner <sebastian.lehner@geosphere.at> `@seblehner <https://github.com/seblehner>`_
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ Announcements
New indicators
^^^^^^^^^^^^^^
* New ``heat_spell_frequency``, ``heat_spell_max_length`` and ``heat_spell_total_length`` : spell length statistics on a bivariate condition that uses the average over a window by default. (:pull:`1885`).
* New ``hot_spell_max_magnitude`` : yields the magnitude of the most intensive heat wave. (:pull:`1926`).
* New ``chill_portion`` and ``chill_unit``: chill portion based on the Dynamic Model and chill unit based on the Utah model indicators. (:issue:`1753`, :pull:`1909`).

New features and enhancements
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* New generic ``xclim.indices.generic.spell_mask`` that returns a mask of which days are part of a spell. Supports multivariate conditions and weights. Used in new generic index ``xclim.indices.generic.bivariate_spell_length_statistics`` that extends ``spell_length_statistics`` to two variables. (:pull:`1885`).
* Indicator parameters can now be assigned a new name, different from the argument name in the compute function. (:pull:`1885`).
* ``xclim.indices.run_length.windowed_max_run_sum`` accumulates positive values across runs and yields the the maximum valued run. (:pull:`1926`).
* Helper function ``xclim.indices.helpers.make_hourly_temperature`` to estimate hourly temperatures from daily min and max temperatures. (:pull:`1909`).

Bug fixes
Expand Down
32 changes: 31 additions & 1 deletion docs/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -2232,7 +2232,7 @@ @article{fishman_chill_1987
abstract = {A two-step model describing the thermal dependence of the dormancy breaking phenomenon is developed. The model assumes that the level of dormancy completion is proportional to the amount of a certain dormancy breaking factor which accumulates in plants by a two-step process. The first step represents a reversible process of formation of a precursor for the dormancy breaking factor at low temperatures and its destruction at high temperatures. The rate constants of this process are assumed to be dependent upon the temperature according to the Arrhenius law. The second step is an irreversible cooperative transition from the unstable precursor to a stable dormancy breaking factor. The transition is assumed to occur when a critical level of the precursor is accumulated. The two-step scheme is analysed mathematically. This model explains qualitatively the main observations on dormancy completion made under controlled temperature conditions and relates the parameters of the theory to the measurable characteristics of the system.}
}

@article { richardson_chill_1974,
@article {richardson_chill_1974,
author = "E. Arlo Richardson and Schuyler D. Seeley and David R. Walker",
title = "A Model for Estimating the Completion of Rest for ‘Redhaven’ and ‘Elberta’ Peach Trees1",
journal = "HortScience",
Expand All @@ -2245,3 +2245,33 @@ @article { richardson_chill_1974
pages= "331 - 332",
url = "https://journals.ashs.org/hortsci/view/journals/hortsci/9/4/article-p331.xml"
}

@article{russo_magnitude_2014,
title = {Magnitude of Extreme Heat Waves in Present Climate and Their Projection in a Warming World},
author = {Russo, Simone and Dosio, Alessandro and Graversen, Rune G. and Sillmann, Jana and Carrao, Hugo and Dunbar, Martha B. and Singleton, Andrew and Montagna, Paolo and Barbola, Paulo and Vogt, J{\"u}rgen V.},
year = {2014},
journal = {Journal of Geophysical Research: Atmospheres},
volume = {119},
number = {22},
pages = {12,500--12,512},
issn = {2169-8996},
doi = {10.1002/2014JD022098},
urldate = {2024-10-03},
langid = {english},
keywords = {climate extremes,climate indices,global models,heat waves}
}

@article{zhang_high_2022,
title = {High {{Sensitivity}} of {{Compound Drought}} and {{Heatwave Events}} to {{Global Warming}} in the {{Future}}},
author = {Zhang, Qin and She, Dunxian and Zhang, Liping and Wang, Gangsheng and Chen, Jie and Hao, Zengchao},
year = {2022},
month = nov,
journal = {Earth's Future},
volume = {10},
number = {11},
pages = {e2022EF002833},
issn = {2328-4277, 2328-4277},
doi = {10.1029/2022EF002833},
urldate = {2024-10-03},
langid = {english}
}
12 changes: 12 additions & 0 deletions tests/test_indices.py
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,18 @@ def test_1d(self, tasmax_series, thresh, window, op, expected):
np.testing.assert_allclose(hsml.values, expected)


class TestHotSpellMaxMagnitude:
def test_simple(self, tasmax_series):
a = np.zeros(365)
a[15:20] += 30 # 5 days
a[40:42] += 50 # too short -> 0
a[86:96] += 30 # at the end and beginning
da = tasmax_series(a + K2C)

out = xci.hot_spell_max_magnitude(da, thresh="25 C", freq="ME")
np.testing.assert_array_equal(out, [25, 0, 30, 20, 0, 0, 0, 0, 0, 0, 0, 0])


class TestTnDays:
def test_above_simple(self, tasmin_series):
a = np.zeros(365)
Expand Down
10 changes: 10 additions & 0 deletions tests/test_run_length.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,16 @@ def test_simple(self, index):
) + len(a[34:45])


class TestWindowedMaxRunSum:
@pytest.mark.parametrize("index", ["first", "last"])
def test_simple(self, index):
a = xr.DataArray(np.zeros(50, float), dims=("time",))
a[4:6] = 5 # too short
a[25:30] = 5 # long enough, but not max
a[35:45] = 5 # max sum => yields 10*5
assert rl.windowed_max_run_sum(a, 3, dim="time", index=index) == 50


class TestLastRun:
@pytest.mark.parametrize(
"coord,expected",
Expand Down
40 changes: 40 additions & 0 deletions tests/test_temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -1597,6 +1597,46 @@ def test_simple(self, tasmax_series):
np.testing.assert_array_equal(out, 8)


class TestHotSpellMaxMagnitude:
def test_simple(self, tasmax_series):
tx = np.zeros(366)
tx[:5] = np.array([30, 30, 30, 30, 30])
tx = tasmax_series(tx + K2C, start="1/1/2000")
hwm = atmos.hot_spell_max_magnitude(tx, freq="YS")
np.testing.assert_array_equal(hwm, [25])

def test_small_window_single_day(self, tasmax_series):
tx = np.zeros(366)
tx[5:8] = np.array([30, 0, 30])
tx = tasmax_series(tx + K2C, start="1/1/2000")
hwm = atmos.hot_spell_max_magnitude(tx, window=1, freq="YS")
np.testing.assert_array_equal(hwm, [5])

def test_small_window_double_day(self, tasmax_series):
tx = np.zeros(366)
tx[5:7] = np.array([30, 30])
tx = tasmax_series(tx + K2C, start="1/1/2000")
hwm = atmos.hot_spell_max_magnitude(tx, window=1, freq="YS")
np.testing.assert_array_equal(hwm, [10])

def test_convert_units(self, tasmax_series):
tx = np.zeros(366)
tx[:5] = np.array([30, 30, 30, 30, 30])
tx = tasmax_series(tx, start="1/1/2000")
tx.attrs["units"] = "C"
hwm = atmos.hot_spell_max_magnitude(tx, freq="YS")
np.testing.assert_array_equal(hwm, [25])

def test_nan_presence(self, tasmax_series):
tx = np.zeros(366)
tx[:5] = np.array([30, 30, 30, 30, 30])
tx[-1] = np.nan
tx = tasmax_series(tx + K2C, start="1/1/2000")

hwm = atmos.hot_spell_max_magnitude(tx, freq="YS")
np.testing.assert_array_equal(hwm, [np.nan])


class TestColdSpellFrequency:
def test_simple(self, tas_series):
a = np.zeros(366)
Expand Down
6 changes: 6 additions & 0 deletions xclim/data/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,12 @@
"title": "Indice de vague de chaleur",
"abstract": "Nombre de jours de vagues de chaleur. Une vague de chaleur se produit lorsque la température maximale quotidienne excède un seuil donné durant un certain nombre de jours."
},
"HOT_SPELL_MAX_MAGNITUDE": {
"long_name": "Différence cumulative maximale entre la température maximale quotidienne et {thresh} pour les jours faisant partie d'une vague de chaleur. Une vague de chaleur est définie comme une série d'au moins {window} jours consécutifs où la température maximale quotidien est au-dessus de {thresh}",
"description": "Cumul {freq:m} maximal de la différence entre la température et {thresh} pour les jours faisant partie d'une vague de chaleur. Une vague de chaleur est définie comme une série d'au moins {window} jours consécutifs où la température maximale quotidien est au-dessus de {thresh}",
"title": "Magnitude de la vague de chaleur {freq:m} la plus intense. Une vague de chaleur est définie comme une série d'au moins {window} jours consécutifs où la température maximale quotidien est au-dessus de {thresh}.",
"abstract": "Magnitude de la vague de chaleur {freq:m} la plus intense. Une vague de chaleur est définie comme une série d'au moins {window} jours consécutifs où la température maximale quotidien est au-dessus de {thresh}."
},
"TG": {
"long_name": "Moyenne des températures maximale et minimale quotidennes",
"description": "Température moyenne quotidienne moyenne estimée par la moyenne des températures maximale et minimale quotidiennnes.",
Expand Down
16 changes: 16 additions & 0 deletions xclim/indicators/atmos/_temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"heating_degree_days",
"hot_spell_frequency",
"hot_spell_max_length",
"hot_spell_max_magnitude",
"hot_spell_total_length",
"huglin_index",
"ice_days",
Expand Down Expand Up @@ -235,6 +236,21 @@ class TempHourlyWithIndexing(ResamplingIndicatorWithIndexing):
compute=indices.heat_wave_frequency,
)

hot_spell_max_magnitude = Temp(
title="Hot spell maximum magnitude",
identifier="hot_spell_max_magnitude",
units="K d",
long_name="Maximum cumulative difference between daily maximum temperature and {thresh} for days within a heat wave. "
"A heat wave is defined as a series of at least {window} consecutive days with daily maximum temperature above {thresh}.",
description="Magnitude of the most intensive heat wave per {freq}. The magnitude is the cumulative exceedance of daily "
"maximum temperature over {thresh}. A heat wave is defined as a series of at least {window} consecutive days with daily "
"maximum temperature above {thresh}",
abstract="Magnitude of the most intensive heat wave per {freq}. A heat wave occurs when daily maximum "
"temperatures exceed given thresholds for a number of days.",
cell_methods="",
compute=indices.hot_spell_max_magnitude,
)

heat_wave_max_length = Temp(
title="Heat wave maximum length",
identifier="heat_wave_max_length",
Expand Down
54 changes: 54 additions & 0 deletions xclim/indices/_threshold.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"heating_degree_days",
"hot_spell_frequency",
"hot_spell_max_length",
"hot_spell_max_magnitude",
"hot_spell_total_length",
"last_snowfall",
"last_spring_frost",
Expand Down Expand Up @@ -1928,6 +1929,59 @@ def heat_wave_index(
return to_agg_units(out, tasmax, "count")


@declare_units(tasmax="[temperature]", thresh="[temperature]")
def hot_spell_max_magnitude(
tasmax: xarray.DataArray,
thresh: Quantified = "25.0 degC",
window: int = 3,
freq: str = "YS",
op: str = ">",
resample_before_rl: bool = True,
) -> xarray.DataArray:
"""Hot spell maximum magnitude.

Magnitude of the most intensive heat wave event as sum of differences between tasmax
and the given threshold for Heat Wave days, defined as three or more consecutive days
over the threshold.

Parameters
----------
tasmax : xarray.DataArray
Maximum daily temperature.
thresh : xarray.DataArray
Threshold temperature on which to designate a heatwave.
window : int
Minimum number of days with temperature above threshold to qualify as a heatwave.
freq : str
Resampling frequency.
op : {">", ">=", "gt", "ge"}
Comparison operation. Default: ">".
resample_before_rl : bool
Determines if the resampling should take place before or after the run
length encoding (or a similar algorithm) is applied to runs.

References
----------
:cite:cts:`russo_magnitude_2014,zhang_high_2022`

Returns
-------
DataArray, [time]
Hot spell maximum magnitude.
"""
thresh = convert_units_to(thresh, tasmax)
over_values = (tasmax - thresh).clip(0)

out = rl.resample_and_rl(
over_values,
resample_before_rl,
rl.windowed_max_run_sum,
window=window,
freq=freq,
)
return to_agg_units(out, tasmax, op="integral")


@declare_units(tas="[temperature]", thresh="[temperature]")
def heating_degree_days(
tas: xarray.DataArray,
Expand Down
62 changes: 58 additions & 4 deletions xclim/indices/run_length.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,15 @@ def rle(
dim: str = "time",
index: str = "first",
) -> xr.DataArray:
"""Generate basic run length function.
"""Run length

Despite its name, this is not an actual run-length encoder : it returns an array of the same shape
as the input with 0 where the input was <= 0, nan where the input was > 0, except on the first (or last) element
of each run of consecutive > 0 values, where it is set to the sum of the elements within the run.
For an actual run length encoder, see :py:func:`rle_1d`.

Usually, the input would be a boolean mask and the first element of each run would then be set to the run's length (thus the name).
But the function also accepts int and float inputs.

Parameters
----------
Expand All @@ -178,9 +186,9 @@ def rle(
Returns
-------
xr.DataArray
Values are 0 where da is False (out of runs).
"""
da = da.astype(int)
if da.dtype == bool:
da = da.astype(int)

# "first" case: Algorithm is applied on inverted array and output is inverted back
if index == "first":
Expand All @@ -192,7 +200,7 @@ def rle(
# Keep total length of each series (and also keep 0's), e.g. 100120123 -> 100N20NN3
# Keep numbers with a 0 to the right and also the last number
cs_s = cs_s.where(da.shift({dim: -1}, fill_value=0) == 0)
out = cs_s.where(da == 1, 0) # Reinsert 0's at their original place
out = cs_s.where(da > 0, 0) # Reinsert 0's at their original place

# Inverting back if needed e.g. 100N20NN3 -> 3NN02N001. This is the output of
# `rle` for 111011001 with index == "first"
Expand Down Expand Up @@ -409,6 +417,50 @@ def windowed_run_count(
return out


def windowed_max_run_sum(
da: xr.DataArray,
window: int,
dim: str = "time",
freq: str | None = None,
index: str = "first",
) -> xr.DataArray:
"""Return the maximum sum of consecutive float values for runs at least as long as the given window length.

Parameters
----------
da : xr.DataArray
Input N-dimensional DataArray (boolean).
window : int
Minimum run length.
When equal to 1, an optimized version of the algorithm is used.
dim : str
Dimension along which to calculate consecutive run (default: 'time').
freq : str
Resampling frequency.
index : {'first', 'last'}
If 'first', the run length is indexed with the first element in the run.
If 'last', with the last element in the run.

Returns
-------
xr.DataArray, [int]
Total number of `True` values part of a consecutive runs of at least `window` long.
"""
if window == 1 and freq is None:
out = rle(da, dim=dim, index=index).max(dim=dim)

else:
d_rse = rle(da, dim=dim, index=index)
d_rle = rle((da > 0).astype(bool), dim=dim, index=index)

d = d_rse.where(d_rle >= window, 0)
if freq is not None:
d = d.resample({dim: freq})
out = d.max(dim=dim)

return out


def _boundary_run(
da: xr.DataArray,
window: int,
Expand Down Expand Up @@ -1203,6 +1255,8 @@ def rle_1d(
) -> tuple[np.array, np.array, np.array]:
"""Return the length, starting position and value of consecutive identical values.

In opposition to py:func:`rle`, this is an actuel run length encoder.

Parameters
----------
arr : Sequence[Union[int, float, bool]]
Expand Down
Loading