Skip to content

Commit

Permalink
Growing Season operators (#1796)
Browse files Browse the repository at this point in the history
### What kind of change does this PR introduce?

* Adds the `op` keyword to `growing_season_{start | end}` indices and
indicators, allowing for customizable threshold operators using
`indices.generic.compare()`.
* Added the `op` operator to the indicator docstrings in English and
French.
* Clarifies a docstring typo in `xclim.indices.growing_season_length`

### Does this PR introduce a breaking change?

Not really. The call signature has changed, but default behaviour is
unchanged.
  • Loading branch information
Zeitsperre authored Jun 27, 2024
2 parents 98c251c + 8e1e57b commit 307cfa3
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 14 deletions.
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ v0.51.0 (unreleased)
--------------------
Contributors to this version: Trevor James Smith (:user:`Zeitsperre`).

New features and enhancements
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* Added the `op` keyword to the `growing_season_{start|end}` indices and indicators, allowing for customizable threshold operators using `indices.generic.compare()`. (:issue:`1794`, :pull:`1796`).

Bug fixes
^^^^^^^^^
* Clarified a typo in the docstring formula for `xclim.indices.growing_season_length`. (:pull:`1796`).

Internal changes
^^^^^^^^^^^^^^^^
* GitHub repository now uses Rulesets for branch protection. (:pull:`1790`).
Expand Down
3 changes: 3 additions & 0 deletions tests/test_indicators.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,9 @@ def test_all_parameters_understood(official_indicators):
if problems - {
("COOL_NIGHT_INDEX", "lat"),
("DRYNESS_INDEX", "lat"),
# TODO: How should we handle the case of Literal[str]?
("GROWING_SEASON_END", "op"),
("GROWING_SEASON_START", "op"),
}:
raise ValueError(
f"The following indicator/parameter couple {problems} use types not listed in InputKind."
Expand Down
6 changes: 3 additions & 3 deletions xclim/data/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -651,8 +651,8 @@
"abstract": "Nombre de jours entre la première occurrence d'une série de jours ayant une température moyenne quotidienne au-dessus d'un seuil et la première occurrence d'une série de jours avec une température moyenne quotidienne sous ce même seuil, survenant après une date de calendrier donnée."
},
"GROWING_SEASON_START": {
"long_name": "Premier jour de la première série de {window} jours ayant une température moyenne quotidienne égale ou au-dessus de {thresh}",
"description": "Jour de l’année marquant le début de la saison de croissance, défini comme le premier jour de la première série de {window} jours ayant une température moyenne quotidienne au-dessus ou égale à {thresh}",
"long_name": "Premier jour de la première série de {window} jours ayant une température moyenne quotidienne {op} {thresh}",
"description": "Jour de l’année marquant le début de la saison de croissance, défini comme le premier jour de la première série de {window} jours ayant une température moyenne quotidienne {op} {thresh}",
"title": "Jour de l'année du début de la saison de croissance",
"abstract": "Premier jour où la température moyenne quotidienne est au-dessus d'un seuil donné depuis un certain nombre de jours consécutifs."
},
Expand Down Expand Up @@ -759,7 +759,7 @@
"abstract": "Nombre maximal de jours consécutifs sans gel où la température minimale quotidienne est au-dessus ou égale à 0°C."
},
"GROWING_SEASON_END": {
"long_name": "Premier jour de la première série de {window} jours ayant une température journalière moyenne sous {thresh}, survenant après {mid_date}",
"long_name": "Premier jour de la première série de {window} jours ayant une température journalière moyenne {op} {thresh}, survenant après {mid_date}",
"description": "Jour de l'année de la fin de la saison de croissance, défini comme le premier jour après {mid_date} ayant une température moyenne quotidienne au-dessus ou égale à {thresh} après une série de {window} jours ayant une température moyenne quotidienne sous {thresh}.",
"title": "Fin de la saison de croissance",
"abstract": "Le premier jour où la température moyenne quotidienne est sous un seuil donné pendant un certain nombre de jours consécutifs après une date de calendrier donnée."
Expand Down
20 changes: 14 additions & 6 deletions xclim/indicators/atmos/_temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -971,13 +971,13 @@ class TempWithIndexing(ResamplingIndicatorWithIndexing):
identifier="growing_season_start",
units="",
standard_name="day_of_year",
long_name="First day of the first series of {window} days with mean daily temperature above or equal to {thresh}",
long_name="First day of the first series of {window} days with mean daily temperature {op} {thresh}",
description="Day of the year marking the beginning of the growing season, defined as the first day of the first "
"series of {window} days with mean daily temperature above or equal to {thresh}.",
"series of {window} days with mean daily temperature {op} {thresh}.",
abstract="The first day when the temperature exceeds a certain threshold for a given number of consecutive days.",
cell_methods="",
compute=indices.growing_season_start,
parameters={"thresh": {"default": "5.0 degC"}},
parameters={"thresh": {"default": "5.0 degC"}, "op": {"default": ">="}},
)

growing_season_length = Temp(
Expand All @@ -996,15 +996,19 @@ class TempWithIndexing(ResamplingIndicatorWithIndexing):
"threshold, occurring after a given calendar date.",
cell_methods="",
compute=indices.growing_season_length,
parameters={"thresh": {"default": "5.0 degC"}, "mid_date": {"default": "07-01"}},
parameters={
"thresh": {"default": "5.0 degC"},
"op": {"default": ">="},
"mid_date": {"default": "07-01"},
},
)

growing_season_end = Temp(
title="Growing season end",
identifier="growing_season_end",
units="",
standard_name="day_of_year",
long_name="First day of the first series of {window} days with mean daily temperature below {thresh}, "
long_name="First day of the first series of {window} days with mean daily temperature {op} {thresh}, "
"occurring after {mid_date}",
description="Day of year of end of growing season, defined as the first day of consistent inferior threshold "
"temperature of {thresh} after a run of {window} days superior to threshold temperature, occurring after "
Expand All @@ -1013,7 +1017,11 @@ class TempWithIndexing(ResamplingIndicatorWithIndexing):
"after a given calendar date.",
cell_methods="",
compute=indices.growing_season_end,
parameters={"thresh": {"default": "5.0 degC"}, "mid_date": {"default": "07-01"}},
parameters={
"thresh": {"default": "5.0 degC"},
"op": {"default": "<"},
"mid_date": {"default": "07-01"},
},
)

tropical_nights = TempWithIndexing(
Expand Down
20 changes: 15 additions & 5 deletions xclim/indices/_threshold.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

import warnings
from typing import Literal

import numpy as np
import xarray
Expand Down Expand Up @@ -978,6 +979,7 @@ def growing_season_start(
thresh: Quantified = "5.0 degC",
window: int = 5,
freq: str = "YS",
op: Literal[">", ">=", "gt", "ge"] = ">=",
) -> xarray.DataArray:
r"""Start of the growing season.
Expand All @@ -994,6 +996,8 @@ def growing_season_start(
Minimum number of days with temperature above threshold needed for evaluation.
freq : str
Resampling frequency.
op : {">", ">=", "gt", "ge"}
Comparison operation. Default: ">=".
Returns
-------
Expand All @@ -1014,8 +1018,9 @@ def growing_season_start(
and :math:`[P]` is 1 if :math:`P` is true, and 0 if false.
"""
thresh = convert_units_to(thresh, tas)
over = tas >= thresh
out = over.resample(time=freq).map(rl.first_run, window=window, coord="dayofyear")
cond = compare(tas, op, thresh, constrain=(">=", ">"))

out = cond.resample(time=freq).map(rl.first_run, window=window, coord="dayofyear")
out.attrs.update(units="", is_dayofyear=np.int32(1), calendar=get_calendar(tas))
return out

Expand All @@ -1027,6 +1032,7 @@ def growing_season_end(
mid_date: DayOfYearStr = "07-01",
window: int = 5,
freq: str = "YS",
op: Literal["<", "<=", "lt", "le"] = "<",
) -> xarray.DataArray:
r"""End of the growing season.
Expand All @@ -1049,6 +1055,8 @@ def growing_season_end(
Minimum number of days with temperature below threshold needed for evaluation.
freq : str
Resampling frequency.
op : {"<", "<=", "lt", "le"}
Comparison operation. Default: "<".
Returns
-------
Expand All @@ -1070,7 +1078,9 @@ def growing_season_end(
and :math:`[P]` is 1 if :math:`P` is true, and 0 if false.
"""
thresh = convert_units_to(thresh, tas)
cond = tas >= thresh

# Note: The following operation is inverted here so that there is less confusion for users.
cond = ~compare(tas, op, thresh, constrain=("<=", "<"))

out = cond.resample(time=freq).map(
rl.run_end_after_date,
Expand Down Expand Up @@ -1130,9 +1140,9 @@ def growing_season_length(
.. math::
TG_{ij} > 5 ℃
TG_{ij} >= 5 ℃
and the first occurrence after 1 July of at least 6 consecutive days with:
and the first occurrence after 1 July of at least six (6) consecutive days with:
.. math::
Expand Down

0 comments on commit 307cfa3

Please sign in to comment.