From ef6fec540cfbe744ce39b44d1e509e81662145cc Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:33:54 -0500 Subject: [PATCH 01/18] add holiday_snow_days and holiday_snow_and_snowfall_days Signed-off-by: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> --- src/xclim/indices/_threshold.py | 150 +++++++++++++++++++++++++++++++- 1 file changed, 146 insertions(+), 4 deletions(-) diff --git a/src/xclim/indices/_threshold.py b/src/xclim/indices/_threshold.py index 9f11be29c..4da2ac250 100644 --- a/src/xclim/indices/_threshold.py +++ b/src/xclim/indices/_threshold.py @@ -9,7 +9,7 @@ import xarray from xclim.core import DayOfYearStr, Quantified -from xclim.core.calendar import doy_from_string, get_calendar +from xclim.core.calendar import doy_from_string, get_calendar, select_time from xclim.core.missing import at_least_n_valid from xclim.core.units import ( convert_units_to, @@ -67,6 +67,8 @@ "growing_season_start", "heat_wave_index", "heating_degree_days", + "holiday_snow_and_snowfall_days", + "holiday_snow_days", "hot_spell_frequency", "hot_spell_max_length", "hot_spell_max_magnitude", @@ -376,7 +378,8 @@ def snd_season_end( window : int Minimum number of days with snow depth below threshold. freq : str - Resampling frequency. The default value is chosen for the northern hemisphere. + Resampling frequency. Default: "YS-JUL". + The default value is chosen for the northern hemisphere. Returns ------- @@ -2766,7 +2769,7 @@ def maximum_consecutive_frost_days( Let :math:`\mathbf{t}=t_0, t_1, \ldots, t_n` be a minimum daily temperature series and :math:`thresh` the threshold below which a day is considered a frost day. Let :math:`\mathbf{s}` be the sorted vector of indices :math:`i` where :math:`[t_i < thresh] \neq [t_{i+1} < thresh]`, that is, the days where the temperature crosses the threshold. - Then the maximum number of consecutive frost days is given by + Then the maximum number of consecutive frost days is given by: .. math:: @@ -2821,7 +2824,7 @@ def maximum_consecutive_dry_days( Let :math:`\mathbf{p}=p_0, p_1, \ldots, p_n` be a daily precipitation series and :math:`thresh` the threshold under which a day is considered dry. Then let :math:`\mathbf{s}` be the sorted vector of indices :math:`i` where :math:`[p_i < thresh] \neq [p_{i+1} < thresh]`, that is, the days where the precipitation crosses the threshold. - Then the maximum number of consecutive dry days is given by + Then the maximum number of consecutive dry days is given by: .. math:: @@ -3633,3 +3636,142 @@ def wet_spell_max_length( resample_before_rl=resample_before_rl, **indexer, ) + + +@declare_units( + snd="[length]", + prsn="[precipitation]", + snd_thresh="[length]", + prsn_thresh="[length]", +) +def holiday_snow_days( + snd: xarray.DataArray, + snd_thresh: Quantified = "20 mm", + date_start: str = "12-25", + date_end: str | None = None, + freq: str = "YS", +) -> xarray.DataArray: # numpydoc ignore=SS05 + r""" + Christmas Days. + + Whether there is a significant amount of snow on the ground on December 25th (or around that time). + + Parameters + ---------- + snd : xarray.DataArray + Surface snow depth. + snd_thresh : Quantified + Threshold snow amount. Default: 20 mm. + date_start : str + Beginning of analysis period. Default: "12-25" (December 25th). + date_end : str, optional + End of analysis period. If not provided, `date_start` is used. + Default: None. + freq : str + Resampling frequency. Default: "YS". + The default value is chosen for the northern hemisphere. + + Returns + ------- + xarray.DataArray, [bool] + Boolean array of years with Christmas Days. + + References + ---------- + https://www.canada.ca/en/environment-climate-change/services/weather-general-tools-resources/historical-christmas-snowfall-data.html + """ + snow_depth = convert_units_to(snd, "mm", context="hydro") + snow_depth_thresh = convert_units_to(snd_thresh, "mm", context="hydro") + snow_depth_constrained = select_time( + snow_depth, + drop=True, + date_bounds=(date_start, date_start if date_end is None else date_end), + ) + + xmas_days = ( + (snow_depth_constrained >= snow_depth_thresh) + .resample(time=freq) + .map(lambda x: x.all(dim="time")) + ) + + xmas_days = xmas_days.assign_attrs({"units": "1"}) + return xmas_days + + +@declare_units( + snd="[length]", + prsn="[precipitation]", + snd_thresh="[length]", + prsn_thresh="[length]", +) +def holiday_snow_and_snowfall_days( + snd: xarray.DataArray, + prsn: xarray.DataArray | None = None, + snd_thresh: Quantified = "20 mm", + prsn_thresh: Quantified = "10 mm", + date_start: str = "12-25", + date_end: str | None = None, + freq: str = "YS-JUL", +) -> xarray.DataArray: + r""" + Perfect Christmas Days. + + Whether there is a significant amount of snow on the ground and snowfall occurring on December 25th. + + Parameters + ---------- + snd : xarray.DataArray + Surface snow depth. + prsn : xarray.DataArray + Snowfall flux. + snd_thresh : Quantified + Threshold snow amount. Default: 20 mm. + prsn_thresh : Quantified + Threshold snowfall flux. Default: 10 mm. + date_start : str + Beginning of analysis period. Default: "12-25" (December 25th). + date_end : str, optional + End of analysis period. If not provided, `date_start` is used. + Default: None. + freq : str + Resampling frequency. Default: "YS-JUL". + The default value is chosen for the northern hemisphere. + + Returns + ------- + xarray.DataArray, [bool] + Boolean array of years with Perfect Christmas Days. + + References + ---------- + https://www.canada.ca/en/environment-climate-change/services/weather-general-tools-resources/historical-christmas-snowfall-data.html + """ + snowfall_rate = rate2amount( + convert_units_to(prsn, "mm/d", context="hydro"), out_units="mm" + ) + snowfall_thresh = convert_units_to(prsn_thresh, "mm", context="hydro") + snowfall_constrained = select_time( + snowfall_rate, + drop=True, + date_bounds=(date_start, date_start if date_end is None else date_end), + ) + + snow_depth = convert_units_to(snd, "mm", context="hydro") + snow_depth_thresh = convert_units_to(snd_thresh, "mm", context="hydro") + snow_depth_constrained = select_time( + snow_depth, + drop=True, + date_bounds=(date_start, date_start if date_end is None else date_end), + ) + + perfect_xmas_days = ( + ( + (snow_depth_constrained >= snow_depth_thresh) + & (snowfall_constrained >= snowfall_thresh) + ) + .resample(time=freq) + .map(lambda x: x.all(dim="time")) + ) + + perfect_xmas_days = perfect_xmas_days.assign_attrs({"units": "1"}) + return perfect_xmas_days From 76123e1f6ecb87451b0f6b5ce2ef9496d90ffaa1 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:45:12 -0500 Subject: [PATCH 02/18] lower default thresh for snowfall Signed-off-by: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> --- src/xclim/indices/_threshold.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/xclim/indices/_threshold.py b/src/xclim/indices/_threshold.py index 4da2ac250..da9227f7b 100644 --- a/src/xclim/indices/_threshold.py +++ b/src/xclim/indices/_threshold.py @@ -3165,7 +3165,7 @@ def degree_days_exceedance_date( ----- Let :math:`TG_{ij}` be the daily mean temperature at day :math:`i` of period :math:`j`, :math:`T` is the reference threshold and :math:`ST` is the sum threshold. Then, starting - at day :math:i_0:, the degree days exceedance date is the first day :math:`k` such that + at day :math:i_0:, the degree days exceedance date is the first day :math:`k` such that: .. math:: @@ -3708,7 +3708,7 @@ def holiday_snow_and_snowfall_days( snd: xarray.DataArray, prsn: xarray.DataArray | None = None, snd_thresh: Quantified = "20 mm", - prsn_thresh: Quantified = "10 mm", + prsn_thresh: Quantified = "1 mm", date_start: str = "12-25", date_end: str | None = None, freq: str = "YS-JUL", @@ -3716,7 +3716,7 @@ def holiday_snow_and_snowfall_days( r""" Perfect Christmas Days. - Whether there is a significant amount of snow on the ground and snowfall occurring on December 25th. + Whether there is a significant amount of snow on the ground and measurable snowfall occurring on December 25th. Parameters ---------- @@ -3727,7 +3727,7 @@ def holiday_snow_and_snowfall_days( snd_thresh : Quantified Threshold snow amount. Default: 20 mm. prsn_thresh : Quantified - Threshold snowfall flux. Default: 10 mm. + Threshold snowfall flux. Default: 1 mm. date_start : str Beginning of analysis period. Default: "12-25" (December 25th). date_end : str, optional From f9e6c5acbb7688a3d34715351b86d2924043ce13 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:48:39 -0500 Subject: [PATCH 03/18] return number of days, add indicator and translation, add tests Signed-off-by: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> --- .github/workflows/main.yml | 2 +- src/xclim/data/fr.json | 12 +++++++ src/xclim/indicators/land/_snow.py | 25 +++++++++++++ src/xclim/indices/_threshold.py | 32 ++++++++--------- tests/test_indices.py | 58 ++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 17 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 353a0014c..83ff9561d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ on: - submitted env: - XCLIM_TESTDATA_BRANCH: v2024.8.23 + XCLIM_TESTDATA_BRANCH: add-snw-prsn concurrency: # For a given workflow, if we push to the same branch, cancel all previous builds on that branch except on main. diff --git a/src/xclim/data/fr.json b/src/xclim/data/fr.json index d42c6b437..79ed46729 100644 --- a/src/xclim/data/fr.json +++ b/src/xclim/data/fr.json @@ -1033,6 +1033,18 @@ "title": "Jours avec neige", "abstract": "Nombre de jours où la neige est entre une borne inférieure et supérieure." }, + "HOLIDAY_SNOW_DAYS": { + "long_name": "Nombre de jours de neige durant les jours fériés", + "description": "Nombre de jours de neige durant les jours fériés.", + "title": "Jours de neige durant les jours fériés", + "abstract": "Nombre de jours de neige durant les jours fériés." + }, + "HOLIDAY_SNOW_AND_SNOWFALL_DAYS": { + "long_name": "Nombre de jours de neige et de jours de chute de neige durant les jours fériés", + "description": "Nombre de jours de neige et de jours de chute de neige durant les jours fériés.", + "title": "Jours de neige et de chute de neige durant les jours fériés", + "abstract": "Nombre de jours de neige et de jours de chute de neige durant les jours fériés." + }, "SND_SEASON_LENGTH": { "long_name": "Durée de couvert de neige", "description": "La saison débute lorsque l'épaisseur de neige est au-dessus de {thresh} durant {window} jours et se termine lorsqu'elle redescend sous {thresh} durant {window} jours.", diff --git a/src/xclim/indicators/land/_snow.py b/src/xclim/indicators/land/_snow.py index ece6aca16..6df82bd25 100644 --- a/src/xclim/indicators/land/_snow.py +++ b/src/xclim/indicators/land/_snow.py @@ -8,6 +8,8 @@ __all__ = [ "blowing_snow", + "holiday_snow_and_snowfall_days", + "holiday_snow_days", "snd_days_above", "snd_max_doy", "snd_season_end", @@ -254,3 +256,26 @@ class SnowWithIndexing(ResamplingIndicatorWithIndexing): abstract="Number of days when the snow amount is greater than or equal to a given threshold.", compute=xci.snw_days_above, ) + +holiday_snow_days = Snow( + title="Christmas snow days", + identifier="holiday_snow_days", + units="1", + long_name="Number of holiday days with snow", + description="The total number of days where snow on the ground was greater than or equal to {snd_thresh} " + "occurring on {date_start} and ending on {date_end}.", + abstract="The total number of days where there is a significant amount of snow on the ground on December 25th.", + compute=xci.holiday_snow_days, +) + +holiday_snow_and_snowfall_days = Snow( + title="Perfect Christmas snow days", + identifier="holiday_snow_and_snowfall_days", + units="1", + long_name="Number of holiday days with snow and snowfall", + description="The total number of days where snow on the ground was greater than or equal to {snd_thresh} " + "and snowfall was greater than or equal to {prsn_thresh} occurring on {date_start} and ending on {date_end}.", + abstract="The total number of days where there is a significant amount of snow on the ground " + "and a measurable snowfall occurring on December 25th.", + compute=xci.holiday_snow_and_snowfall_days, +) diff --git a/src/xclim/indices/_threshold.py b/src/xclim/indices/_threshold.py index da9227f7b..f5d8cc426 100644 --- a/src/xclim/indices/_threshold.py +++ b/src/xclim/indices/_threshold.py @@ -3680,8 +3680,8 @@ def holiday_snow_days( ---------- https://www.canada.ca/en/environment-climate-change/services/weather-general-tools-resources/historical-christmas-snowfall-data.html """ - snow_depth = convert_units_to(snd, "mm", context="hydro") - snow_depth_thresh = convert_units_to(snd_thresh, "mm", context="hydro") + snow_depth = convert_units_to(snd, "m") + snow_depth_thresh = convert_units_to(snd_thresh, "m") snow_depth_constrained = select_time( snow_depth, drop=True, @@ -3691,7 +3691,7 @@ def holiday_snow_days( xmas_days = ( (snow_depth_constrained >= snow_depth_thresh) .resample(time=freq) - .map(lambda x: x.all(dim="time")) + .map(lambda x: x.sum(dim="time")) ) xmas_days = xmas_days.assign_attrs({"units": "1"}) @@ -3708,7 +3708,7 @@ def holiday_snow_and_snowfall_days( snd: xarray.DataArray, prsn: xarray.DataArray | None = None, snd_thresh: Quantified = "20 mm", - prsn_thresh: Quantified = "1 mm", + prsn_thresh: Quantified = "1 cm", date_start: str = "12-25", date_end: str | None = None, freq: str = "YS-JUL", @@ -3739,15 +3739,23 @@ def holiday_snow_and_snowfall_days( Returns ------- - xarray.DataArray, [bool] - Boolean array of years with Perfect Christmas Days. + xarray.DataArray, [int] + The total number of days with snow and snowfall during the holiday. References ---------- https://www.canada.ca/en/environment-climate-change/services/weather-general-tools-resources/historical-christmas-snowfall-data.html """ + snow_depth = convert_units_to(snd, "m") + snow_depth_thresh = convert_units_to(snd_thresh, "m") + snow_depth_constrained = select_time( + snow_depth, + drop=True, + date_bounds=(date_start, date_start if date_end is None else date_end), + ) + snowfall_rate = rate2amount( - convert_units_to(prsn, "mm/d", context="hydro"), out_units="mm" + convert_units_to(prsn, "mm day-1", context="hydro"), out_units="mm" ) snowfall_thresh = convert_units_to(prsn_thresh, "mm", context="hydro") snowfall_constrained = select_time( @@ -3756,21 +3764,13 @@ def holiday_snow_and_snowfall_days( date_bounds=(date_start, date_start if date_end is None else date_end), ) - snow_depth = convert_units_to(snd, "mm", context="hydro") - snow_depth_thresh = convert_units_to(snd_thresh, "mm", context="hydro") - snow_depth_constrained = select_time( - snow_depth, - drop=True, - date_bounds=(date_start, date_start if date_end is None else date_end), - ) - perfect_xmas_days = ( ( (snow_depth_constrained >= snow_depth_thresh) & (snowfall_constrained >= snowfall_thresh) ) .resample(time=freq) - .map(lambda x: x.all(dim="time")) + .map(lambda x: x.sum(dim="time")) ) perfect_xmas_days = perfect_xmas_days.assign_attrs({"units": "1"}) diff --git a/tests/test_indices.py b/tests/test_indices.py index 22978b49d..932909198 100644 --- a/tests/test_indices.py +++ b/tests/test_indices.py @@ -1468,6 +1468,64 @@ def test_1d( np.testing.assert_allclose(hwml.values, expected) +class TestHolidayIndices: + + def test_xmas_days_simple(self, snd_series): + # 5ish years of data + snd = snd_series(np.zeros(365 * 5), units="cm") + + # add snow on ground on December 25 for first 3 years + snd.loc["2000-12-25"] = 2 + snd.loc["2001-12-25"] = 1.5 # not enough + snd.loc["2002-12-25"] = 2 + snd.loc["2003-12-25"] = 0 # no snow + snd.loc["2004-12-25"] = 6 + + out = xci.holiday_snow_days(snd) + np.testing.assert_array_equal(out, [1, 0, 1, 0, 1]) + + def test_xmas_days_range(self, snd_series): + # 5ish years of data + snd = snd_series(np.zeros(365 * 5), units="cm") + + # add snow on ground on December 25 for first 3 years + snd.loc["2000-12-25"] = 2 + snd.loc["2001-12-25"] = 1.5 # not enough + snd.loc["2002-12-24"] = 10 # a réveillon miracle + snd.loc["2002-12-25"] = 2 + snd.loc["2003-12-25"] = 0 # no snow + snd.loc["2004-12-25"] = 6 + + out = xci.holiday_snow_days(snd, date_start="12-24", date_end="12-25") + np.testing.assert_array_equal(out, [1, 0, 2, 0, 1]) + + def test_perfect_xmas_days_simple(self, snd_series, prsn_series): + # 5ish years of data + a = np.zeros(365 * 5) + snd = snd_series(a, units="mm") + prsn = prsn_series(a.copy(), units="cm day-1") + + # add snow on ground on December 25 + snd.loc["2000-12-25"] = 20 + snd.loc["2001-12-25"] = 15 # not enough + snd.loc["2001-12-26"] = 30 # too bad it's Boxing Day + snd.loc["2002-12-25"] = 20 + snd.loc["2003-12-25"] = 0 # no snow + snd.loc["2004-12-25"] = 60 + + # add snowfall on December 25 + prsn.loc["2000-12-25"] = 5 + prsn.loc["2001-12-25"] = 2 + prsn.loc["2001-12-26"] = 30 # too bad it's Boxing Day + prsn.loc["2002-12-25"] = 0.5 # not quite enough + prsn.loc["2003-12-25"] = 0 # no snow + prsn.loc["2004-12-25"] = 10 + prsn = convert_units_to(prsn, "kg m-2 s-1", context="hydro") + + out = xci.holiday_snow_and_snowfall_days(snd, prsn) + np.testing.assert_array_equal(out, [1, 0, 0, 0, 1]) + + class TestHotSpellFrequency: @pytest.mark.parametrize( "thresh,window,op,expected", From 03d24cad10e9a960090bcb7199f4c7e236ac514e Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:44:17 -0500 Subject: [PATCH 04/18] add holiday snow indicator tests Signed-off-by: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> --- tests/test_snow.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/test_snow.py b/tests/test_snow.py index 2144f757f..505aca50e 100644 --- a/tests/test_snow.py +++ b/tests/test_snow.py @@ -2,6 +2,7 @@ import numpy as np import pytest +import xarray as xr from xclim import land from xclim.core import ValidationError @@ -120,3 +121,61 @@ def test_simple(self, snw_series): snw = snw_series(a, start="2001-01-01") out = land.snw_max_doy(snw, freq="YS") np.testing.assert_array_equal(out, [21, np.nan]) + + +class TestHolidaySnowIndicators: + + def test_xmas_days_simple(self, nimbus): + ds = xr.open_dataset( + nimbus.fetch( + "cmip6/snw_day_CanESM5_historical_r1i1p1f1_gn_19910101-20101231.nc" + ) + ) + snd = land.snw_to_snd(ds.snw) + + out = land.holiday_snow_days(snd) + + assert out.units == "1" + assert out.long_name == "Number of holiday days with snow" + np.testing.assert_array_equal( + out.sum(dim="time"), + [ + [7.0, 5.0, 2.0, 0.0, 0.0], + [14.0, 13.0, 9.0, 6.0, 2.0], + [18.0, 19.0, 19.0, 18.0, 13.0], + [20.0, 20.0, 20.0, 20.0, 20.0], + [20.0, 20.0, 20.0, 20.0, 20.0], + [20.0, 20.0, 20.0, 20.0, 20.0], + ], + ) + + def test_perfect_xmas_days_simple(self, nimbus): + ds_snw = xr.open_dataset( + nimbus.fetch( + "cmip6/snw_day_CanESM5_historical_r1i1p1f1_gn_19910101-20101231.nc" + ) + ) + ds_prsn = xr.open_dataset( + nimbus.fetch( + "cmip6/prsn_day_CanESM5_historical_r1i1p1f1_gn_19910101-20101231.nc" + ) + ) + + snd = land.snw_to_snd(ds_snw.snw) + prsn = ds_prsn.prsn + + out = land.holiday_snow_and_snowfall_days(snd, prsn) + + assert out.units == "1" + assert out.long_name == "Number of holiday days with snow and snowfall" + np.testing.assert_array_equal( + out.sum(dim="time"), + [ + [0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0], + [1.0, 0.0, 0.0, 0.0, 1.0], + [0.0, 0.0, 1.0, 1.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 0.0], + ], + ) From fc691a2fbdcd4855919acfabe453eb23a54ef6fb Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:56:24 -0500 Subject: [PATCH 05/18] update CHANGELOG.rst Signed-off-by: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> --- CHANGELOG.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a4d922613..7a66fc4db 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,16 @@ Changelog ========= + +v0.55.0 (unreleased) +-------------------- +Contributors to this version: Trevor James Smith (:user:`Zeitsperre`). + +New indicators +^^^^^^^^^^^^^^ +* Added ``xclim.indices.holiday_snow_days`` to compute the number of days with snow on the ground during holidays ("Christmas Days"). (:issue:`2029`, :pull:`2030`). +* Added ``xclim.indices.holiday_snow_and_snowfall_days`` to compute the number of days with snow on the ground and measurable snowfall during holidays ("Perfect Christmas Days"). (:issue:`2029`, :pull:`2030`). + v0.54.0 (2024-12-16) -------------------- Contributors to this version: Trevor James Smith (:user:`Zeitsperre`), Pascal Bourgault (:user:`aulemahal`), Éric Dupuis (:user:`coxipi`), Sascha Hofmann (:user:`saschahofmann`). From 320cda154c04e6e707d789c7717e3c16832ee15b Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Wed, 8 Jan 2025 12:38:48 -0500 Subject: [PATCH 06/18] Apply suggestions from code review Co-authored-by: Pascal Bourgault --- src/xclim/indicators/land/_snow.py | 4 ++-- src/xclim/indices/_threshold.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/xclim/indicators/land/_snow.py b/src/xclim/indicators/land/_snow.py index 6df82bd25..f4dd714ae 100644 --- a/src/xclim/indicators/land/_snow.py +++ b/src/xclim/indicators/land/_snow.py @@ -260,7 +260,7 @@ class SnowWithIndexing(ResamplingIndicatorWithIndexing): holiday_snow_days = Snow( title="Christmas snow days", identifier="holiday_snow_days", - units="1", + units="days", long_name="Number of holiday days with snow", description="The total number of days where snow on the ground was greater than or equal to {snd_thresh} " "occurring on {date_start} and ending on {date_end}.", @@ -271,7 +271,7 @@ class SnowWithIndexing(ResamplingIndicatorWithIndexing): holiday_snow_and_snowfall_days = Snow( title="Perfect Christmas snow days", identifier="holiday_snow_and_snowfall_days", - units="1", + units="days", long_name="Number of holiday days with snow and snowfall", description="The total number of days where snow on the ground was greater than or equal to {snd_thresh} " "and snowfall was greater than or equal to {prsn_thresh} occurring on {date_start} and ending on {date_end}.", diff --git a/src/xclim/indices/_threshold.py b/src/xclim/indices/_threshold.py index f5d8cc426..d819f31ce 100644 --- a/src/xclim/indices/_threshold.py +++ b/src/xclim/indices/_threshold.py @@ -3691,7 +3691,7 @@ def holiday_snow_days( xmas_days = ( (snow_depth_constrained >= snow_depth_thresh) .resample(time=freq) - .map(lambda x: x.sum(dim="time")) + .sum() ) xmas_days = xmas_days.assign_attrs({"units": "1"}) @@ -3770,7 +3770,7 @@ def holiday_snow_and_snowfall_days( & (snowfall_constrained >= snowfall_thresh) ) .resample(time=freq) - .map(lambda x: x.sum(dim="time")) + .sum() ) perfect_xmas_days = perfect_xmas_days.assign_attrs({"units": "1"}) From 86554e4d48c5376ae84e909c42cf9c6171e9850e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 17:40:05 +0000 Subject: [PATCH 07/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/xclim/indices/_threshold.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/xclim/indices/_threshold.py b/src/xclim/indices/_threshold.py index d819f31ce..6e30b311d 100644 --- a/src/xclim/indices/_threshold.py +++ b/src/xclim/indices/_threshold.py @@ -3688,11 +3688,7 @@ def holiday_snow_days( date_bounds=(date_start, date_start if date_end is None else date_end), ) - xmas_days = ( - (snow_depth_constrained >= snow_depth_thresh) - .resample(time=freq) - .sum() - ) + xmas_days = (snow_depth_constrained >= snow_depth_thresh).resample(time=freq).sum() xmas_days = xmas_days.assign_attrs({"units": "1"}) return xmas_days From 04787cbd3a6198171aa97757af2f56d32597bee9 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Wed, 8 Jan 2025 12:42:22 -0500 Subject: [PATCH 08/18] fix units Signed-off-by: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> --- src/xclim/indices/_threshold.py | 4 ++-- tests/test_snow.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/xclim/indices/_threshold.py b/src/xclim/indices/_threshold.py index 6e30b311d..4404461c7 100644 --- a/src/xclim/indices/_threshold.py +++ b/src/xclim/indices/_threshold.py @@ -3690,7 +3690,7 @@ def holiday_snow_days( xmas_days = (snow_depth_constrained >= snow_depth_thresh).resample(time=freq).sum() - xmas_days = xmas_days.assign_attrs({"units": "1"}) + xmas_days = xmas_days.assign_attrs({"units": "days"}) return xmas_days @@ -3769,5 +3769,5 @@ def holiday_snow_and_snowfall_days( .sum() ) - perfect_xmas_days = perfect_xmas_days.assign_attrs({"units": "1"}) + perfect_xmas_days = perfect_xmas_days.assign_attrs({"units": "days"}) return perfect_xmas_days diff --git a/tests/test_snow.py b/tests/test_snow.py index 505aca50e..7158c58d8 100644 --- a/tests/test_snow.py +++ b/tests/test_snow.py @@ -135,7 +135,7 @@ def test_xmas_days_simple(self, nimbus): out = land.holiday_snow_days(snd) - assert out.units == "1" + assert out.units == "days" assert out.long_name == "Number of holiday days with snow" np.testing.assert_array_equal( out.sum(dim="time"), @@ -166,7 +166,7 @@ def test_perfect_xmas_days_simple(self, nimbus): out = land.holiday_snow_and_snowfall_days(snd, prsn) - assert out.units == "1" + assert out.units == "days" assert out.long_name == "Number of holiday days with snow and snowfall" np.testing.assert_array_equal( out.sum(dim="time"), From 0050cfe0dc3c6b5436ea7c630a0e67e545d144a7 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:55:26 -0500 Subject: [PATCH 09/18] update testdata pin Signed-off-by: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 83ff9561d..b24ab8ec3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ on: - submitted env: - XCLIM_TESTDATA_BRANCH: add-snw-prsn + XCLIM_TESTDATA_BRANCH: v2025.1.8 concurrency: # For a given workflow, if we push to the same branch, cancel all previous builds on that branch except on main. From 2484976872ab94b4290aa9651770f684e54fed12 Mon Sep 17 00:00:00 2001 From: Zeitsperre <10819524+Zeitsperre@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:27:18 -0500 Subject: [PATCH 10/18] add bivariate_count_occurrences, rework logic to use generic indices Signed-off-by: Zeitsperre <10819524+Zeitsperre@users.noreply.github.com> --- src/xclim/indices/_threshold.py | 58 ++++++++++++++---------- src/xclim/indices/generic.py | 78 ++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 25 deletions(-) diff --git a/src/xclim/indices/_threshold.py b/src/xclim/indices/_threshold.py index 4404461c7..7fac2949c 100644 --- a/src/xclim/indices/_threshold.py +++ b/src/xclim/indices/_threshold.py @@ -22,7 +22,9 @@ ) from xclim.indices import run_length as rl from xclim.indices.generic import ( + bivariate_count_occurrences, compare, + count_occurrences, cumulative_difference, domain_count, first_day_threshold_reached, @@ -3457,7 +3459,7 @@ def wet_spell_frequency( Resampling frequency. 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. - op : {"sum","min", "max", "mean"} + op : {"sum", "min", "max", "mean"} Operation to perform on the window. Default is "sum", which checks that the sum of accumulated precipitation over the whole window is more than the threshold. @@ -3647,14 +3649,15 @@ def wet_spell_max_length( def holiday_snow_days( snd: xarray.DataArray, snd_thresh: Quantified = "20 mm", + op: str = ">=", date_start: str = "12-25", date_end: str | None = None, freq: str = "YS", ) -> xarray.DataArray: # numpydoc ignore=SS05 - r""" + """ Christmas Days. - Whether there is a significant amount of snow on the ground on December 25th (or around that time). + Whether there is a significant amount of snow on the ground on December 25th (or a given date range). Parameters ---------- @@ -3662,6 +3665,8 @@ def holiday_snow_days( Surface snow depth. snd_thresh : Quantified Threshold snow amount. Default: 20 mm. + op : {">", "gt", ">=", "ge"} + Comparison operation. Default: ">=". date_start : str Beginning of analysis period. Default: "12-25" (December 25th). date_end : str, optional @@ -3680,15 +3685,15 @@ def holiday_snow_days( ---------- https://www.canada.ca/en/environment-climate-change/services/weather-general-tools-resources/historical-christmas-snowfall-data.html """ - snow_depth = convert_units_to(snd, "m") - snow_depth_thresh = convert_units_to(snd_thresh, "m") - snow_depth_constrained = select_time( - snow_depth, + snd_constrained = select_time( + snd, drop=True, date_bounds=(date_start, date_start if date_end is None else date_end), ) - xmas_days = (snow_depth_constrained >= snow_depth_thresh).resample(time=freq).sum() + xmas_days = count_occurrences( + snd_constrained, snd_thresh, freq, op, constrain=[">=", ">"] + ) xmas_days = xmas_days.assign_attrs({"units": "days"}) return xmas_days @@ -3705,6 +3710,8 @@ def holiday_snow_and_snowfall_days( prsn: xarray.DataArray | None = None, snd_thresh: Quantified = "20 mm", prsn_thresh: Quantified = "1 cm", + snd_op: str = ">=", + prsn_op: str = ">=", date_start: str = "12-25", date_end: str | None = None, freq: str = "YS-JUL", @@ -3724,6 +3731,10 @@ def holiday_snow_and_snowfall_days( Threshold snow amount. Default: 20 mm. prsn_thresh : Quantified Threshold snowfall flux. Default: 1 mm. + snd_op : {">", "gt", ">=", "ge"} + Comparison operation for snow depth. Default: ">=". + prsn_op : {">", "gt", ">=", "ge"} + Comparison operation for snowfall flux. Default: ">=". date_start : str Beginning of analysis period. Default: "12-25" (December 25th). date_end : str, optional @@ -3742,31 +3753,32 @@ def holiday_snow_and_snowfall_days( ---------- https://www.canada.ca/en/environment-climate-change/services/weather-general-tools-resources/historical-christmas-snowfall-data.html """ - snow_depth = convert_units_to(snd, "m") - snow_depth_thresh = convert_units_to(snd_thresh, "m") - snow_depth_constrained = select_time( - snow_depth, + snd_constrained = select_time( + snd, drop=True, date_bounds=(date_start, date_start if date_end is None else date_end), ) - snowfall_rate = rate2amount( + prsn_mm = rate2amount( convert_units_to(prsn, "mm day-1", context="hydro"), out_units="mm" ) - snowfall_thresh = convert_units_to(prsn_thresh, "mm", context="hydro") - snowfall_constrained = select_time( - snowfall_rate, + prsn_mm_constrained = select_time( + prsn_mm, drop=True, date_bounds=(date_start, date_start if date_end is None else date_end), ) - perfect_xmas_days = ( - ( - (snow_depth_constrained >= snow_depth_thresh) - & (snowfall_constrained >= snowfall_thresh) - ) - .resample(time=freq) - .sum() + perfect_xmas_days = bivariate_count_occurrences( + data_var1=snd_constrained, + data_var2=prsn_mm_constrained, + threshold_var1=snd_thresh, + threshold_var2=prsn_thresh, + op_var1=snd_op, + op_var2=prsn_op, + freq=freq, + var_reducer="all", + constrain_var1=[">=", ">"], + constrain_var2=[">=", ">"], ) perfect_xmas_days = perfect_xmas_days.assign_attrs({"units": "days"}) diff --git a/src/xclim/indices/generic.py b/src/xclim/indices/generic.py index e9dd96920..f8ead5977 100644 --- a/src/xclim/indices/generic.py +++ b/src/xclim/indices/generic.py @@ -34,6 +34,7 @@ __all__ = [ "aggregate_between_dates", "binary_ops", + "bivariate_count_occurrences", "bivariate_spell_length_statistics", "compare", "count_level_crossings", @@ -903,9 +904,9 @@ def count_occurrences( """ Calculate the number of times some condition is met. - First, the threshold is transformed to the same standard_name and units as the input data: + First, the threshold is transformed to the same standard_name and units as the input data; Then the thresholding is performed as condition(data, threshold), - i.e. if condition is `<`, then this counts the number of times `data < threshold`: + i.e. if condition is `<`, then this counts the number of times `data < threshold`; Finally, count the number of occurrences when condition is met. Parameters @@ -934,6 +935,79 @@ def count_occurrences( return to_agg_units(out, data, "count", dim="time") +@declare_relative_units(threshold_var1="", threshold_var2="") +def bivariate_count_occurrences( + *, + data_var1: xr.DataArray, + data_var2: xr.DataArray, + threshold_var1: Quantified, + threshold_var2: Quantified, + freq: str, + op_var1: str, + op_var2: str, + var_reducer: str, + constrain_var1: Sequence[str] | None = None, + constrain_var2: Sequence[str] | None = None, +) -> xr.DataArray: + """ + Calculate the number of times some conditions are met for two variables. + + First, the thresholds are transformed to the same standard_name and units as their corresponding input data; + Then the thresholding is performed as condition(data, threshold) for each variable, + i.e. if condition is `<`, then this counts the number of times `data < threshold`; + Then the conditions are combined according to `var_reducer`; + Finally, the number of occurrences where conditions are met for "all" or "any" events are counted. + + Parameters + ---------- + data_var1 : xr.DataArray + An array. + data_var2 : xr.DataArray + An array. + threshold_var1 : Quantified + Threshold for data variable 1. + threshold_var2 : Quantified + Threshold for data variable 2. + freq : str + Resampling frequency defining the periods as defined in :ref:`timeseries.resampling`. + op_var1 : {">", "gt", "<", "lt", ">=", "ge", "<=", "le", "==", "eq", "!=", "ne"} + Logical operator for data variable 1. e.g. arr > thresh. + op_var2 : {">", "gt", "<", "lt", ">=", "ge", "<=", "le", "==", "eq", "!=", "ne"} + Logical operator for data variable 2. e.g. arr > thresh. + var_reducer : {"all", "any"} + The condition must either be fulfilled on *all* or *any* variables for the period to be considered an occurrence. + constrain_var1 : sequence of str, optional + Optionally allowed comparison operators for variable 1. + constrain_var2 : sequence of str, optional + Optionally allowed comparison operators for variable 2. + + Returns + ------- + xr.DataArray + The DataArray of counted occurrences. + + Notes + ----- + Sampling and variable units are derived from `data_var1`. + """ + threshold_var1 = convert_units_to(threshold_var1, data_var1) + threshold_var2 = convert_units_to(threshold_var2, data_var2) + + cond_var1 = compare(data_var1, op_var1, threshold_var1, constrain_var1) + cond_var2 = compare(data_var2, op_var2, threshold_var2, constrain_var2) + + if var_reducer == "all": + cond = cond_var1 & cond_var2 + elif var_reducer == "any": + cond = cond_var1 | cond_var2 + else: + raise ValueError(f"Unsupported value for var_reducer: {var_reducer}") + + out = cond.resample(time=freq).sum() + + return to_agg_units(out, data_var1, "count", dim="time") + + def diurnal_temperature_range( low_data: xr.DataArray, high_data: xr.DataArray, reducer: str, freq: str ) -> xr.DataArray: From 9a1760df39cfa6f8931fdefd020b2e13b2e0d840 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:48:55 -0500 Subject: [PATCH 11/18] Apply suggestions from code review Co-authored-by: Pascal Bourgault --- src/xclim/data/fr.json | 16 ++++++++-------- src/xclim/indices/_threshold.py | 9 +++------ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/xclim/data/fr.json b/src/xclim/data/fr.json index 79ed46729..548eb3cfb 100644 --- a/src/xclim/data/fr.json +++ b/src/xclim/data/fr.json @@ -1034,16 +1034,16 @@ "abstract": "Nombre de jours où la neige est entre une borne inférieure et supérieure." }, "HOLIDAY_SNOW_DAYS": { - "long_name": "Nombre de jours de neige durant les jours fériés", - "description": "Nombre de jours de neige durant les jours fériés.", - "title": "Jours de neige durant les jours fériés", - "abstract": "Nombre de jours de neige durant les jours fériés." + "long_name": "Nombre de jours de neige durant les jours de Noël", + "description": "Nombre de jours de neige durant les jours de Noël.", + "title": "Jours de neige durant les jours de Noël", + "abstract": "Nombre de jours de neige durant les jours de Noël." }, "HOLIDAY_SNOW_AND_SNOWFALL_DAYS": { - "long_name": "Nombre de jours de neige et de jours de chute de neige durant les jours fériés", - "description": "Nombre de jours de neige et de jours de chute de neige durant les jours fériés.", - "title": "Jours de neige et de chute de neige durant les jours fériés", - "abstract": "Nombre de jours de neige et de jours de chute de neige durant les jours fériés." + "long_name": "Nombre de jours de neige et de jours de chute de neige durant les jours de Noël", + "description": "Nombre de jours de neige et de jours de chute de neige durant les jours de Noël.", + "title": "Jours de neige et de chute de neige durant les jours de Noël", + "abstract": "Nombre de jours de neige et de jours de chute de neige durant les jours de Noël." }, "SND_SEASON_LENGTH": { "long_name": "Durée de couvert de neige", diff --git a/src/xclim/indices/_threshold.py b/src/xclim/indices/_threshold.py index 7fac2949c..8bbca22e7 100644 --- a/src/xclim/indices/_threshold.py +++ b/src/xclim/indices/_threshold.py @@ -3687,7 +3687,6 @@ def holiday_snow_days( """ snd_constrained = select_time( snd, - drop=True, date_bounds=(date_start, date_start if date_end is None else date_end), ) @@ -3695,7 +3694,7 @@ def holiday_snow_days( snd_constrained, snd_thresh, freq, op, constrain=[">=", ">"] ) - xmas_days = xmas_days.assign_attrs({"units": "days"}) + xmas_days = to_agg_units(xmas_days, snd, "count") return xmas_days @@ -3709,7 +3708,7 @@ def holiday_snow_and_snowfall_days( snd: xarray.DataArray, prsn: xarray.DataArray | None = None, snd_thresh: Quantified = "20 mm", - prsn_thresh: Quantified = "1 cm", + prsn_thresh: Quantified = "1 mm", snd_op: str = ">=", prsn_op: str = ">=", date_start: str = "12-25", @@ -3755,7 +3754,6 @@ def holiday_snow_and_snowfall_days( """ snd_constrained = select_time( snd, - drop=True, date_bounds=(date_start, date_start if date_end is None else date_end), ) @@ -3764,7 +3762,6 @@ def holiday_snow_and_snowfall_days( ) prsn_mm_constrained = select_time( prsn_mm, - drop=True, date_bounds=(date_start, date_start if date_end is None else date_end), ) @@ -3781,5 +3778,5 @@ def holiday_snow_and_snowfall_days( constrain_var2=[">=", ">"], ) - perfect_xmas_days = perfect_xmas_days.assign_attrs({"units": "days"}) + perfect_xmas_days = to_agg_units(perfect_xmas_days, snd, "count") return perfect_xmas_days From 8e8765afcb79423e6fedd1ecae6207e112fe02e3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:49:48 +0000 Subject: [PATCH 12/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/xclim/indices/_threshold.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xclim/indices/_threshold.py b/src/xclim/indices/_threshold.py index 8bbca22e7..afb2e6ba3 100644 --- a/src/xclim/indices/_threshold.py +++ b/src/xclim/indices/_threshold.py @@ -3694,7 +3694,7 @@ def holiday_snow_days( snd_constrained, snd_thresh, freq, op, constrain=[">=", ">"] ) - xmas_days = to_agg_units(xmas_days, snd, "count") + xmas_days = to_agg_units(xmas_days, snd, "count") return xmas_days From 86e952ea1ff0f94f2b43eb13f4f0ceb8e9808784 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 13 Jan 2025 10:31:59 -0500 Subject: [PATCH 13/18] Update src/xclim/indices/_threshold.py Co-authored-by: Pascal Bourgault --- src/xclim/indices/_threshold.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xclim/indices/_threshold.py b/src/xclim/indices/_threshold.py index afb2e6ba3..a3580b212 100644 --- a/src/xclim/indices/_threshold.py +++ b/src/xclim/indices/_threshold.py @@ -3729,7 +3729,7 @@ def holiday_snow_and_snowfall_days( snd_thresh : Quantified Threshold snow amount. Default: 20 mm. prsn_thresh : Quantified - Threshold snowfall flux. Default: 1 mm. + Threshold daily snowfall liquid-water equivalent thickness. Default: 1 mm. snd_op : {">", "gt", ">=", "ge"} Comparison operation for snow depth. Default: ">=". prsn_op : {">", "gt", ">=", "ge"} From 7241bf3e7f38399eb2230aa10c7ffa16545fe724 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:28:42 -0500 Subject: [PATCH 14/18] adjust tests, update testdata registry and tag Signed-off-by: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> --- src/xclim/testing/registry.txt | 46 ++++++++++++++++--------------- src/xclim/testing/utils.py | 2 +- tests/test_indices.py | 49 +++++++++++++++++++++++----------- tests/test_snow.py | 12 ++++----- 4 files changed, 65 insertions(+), 44 deletions(-) diff --git a/src/xclim/testing/registry.txt b/src/xclim/testing/registry.txt index ec0fbbfd4..5a219a55d 100644 --- a/src/xclim/testing/registry.txt +++ b/src/xclim/testing/registry.txt @@ -1,6 +1,28 @@ +CRCM5/tasmax_bby_198406_se.nc sha256:9a80cc19ed212428ef90ce0cc40790fbf0d1fc301df0abdf578da45843dae93d CanESM2_365day/pr_day_CanESM2_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc sha256:16dafec260dd74bf38f87482baa34cc35a1689facfb5557ebfc7d2c928618fc7 CanESM2_365day/tasmax_day_CanESM2_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc sha256:0c57c56e38a9e5b0623180c3def9406e9ddabbe7b1c01b282f1a34c4a61ea357 CanESM2_365day/tasmin_day_CanESM2_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc sha256:5d43ec47759bf9d118942277fe8d7c632765c3a0ba02dc828b0610e1f2030a63 +ERA5/daily_surface_cancities_1990-1993.nc sha256:049d54ace3d229a96cc621189daa3e1a393959ab8d988221cfc7b2acd7ab94b2 +EnsembleReduce/TestEnsReduceCriteria.nc sha256:ae7a70b9d5c54ab072f1cfbfab91d430a41c5067db3c1968af57ea2122cfe8e7 +EnsembleStats/BCCAQv2+ANUSPLIN300_ACCESS1-0_historical+rcp45_r1i1p1_1950-2100_tg_mean_YS.nc sha256:ca0cc893cf91db7c6dfe3df10d605684eabbea55b7e26077c10142d302e55aed +EnsembleStats/BCCAQv2+ANUSPLIN300_BNU-ESM_historical+rcp45_r1i1p1_1950-2100_tg_mean_YS.nc sha256:c796276f563849c31bf388a3beb4a440eeb72062a84b4cf9760c854d1e990ca4 +EnsembleStats/BCCAQv2+ANUSPLIN300_CCSM4_historical+rcp45_r1i1p1_1950-2100_tg_mean_YS.nc sha256:9cfa9bc4e81e936eb680a55db428ccd9f0a6d366d4ae2c4a9064bfa5d71e5ca7 +EnsembleStats/BCCAQv2+ANUSPLIN300_CCSM4_historical+rcp45_r2i1p1_1950-2100_tg_mean_YS.nc sha256:ca36aafb3c63ddb6bfc8537abb854b71f719505c1145d5c81c3315eb1a13647c +EnsembleStats/BCCAQv2+ANUSPLIN300_CNRM-CM5_historical+rcp45_r1i1p1_1970-2050_tg_mean_YS.nc sha256:623eab96d75d8cc8abd59dfba1c14cfb06fd7c0fe9ce86788d3c8b0891684df2 +FWI/GFWED_sample_2017.nc sha256:cf3bde795825663894fa7619a028d5a14fee307c623968235f25393f7afe159e +FWI/cffdrs_test_fwi.nc sha256:147be24e080aa67f17261f61f05a5dfb381a66a23785a327e47e2303667ca3ab +FWI/cffdrs_test_wDC.nc sha256:ebadcad1dd6a1a1e93c29a1143d7caefd46593ea2fbeb721015245981cce90c3 +HadGEM2-CC_360day/pr_day_HadGEM2-CC_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc sha256:c45ff4c17ba9fd92392bb08a7705789071a0bec40bde48f5a838ff12413cc33b +HadGEM2-CC_360day/tasmax_day_HadGEM2-CC_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc sha256:aa3eb54ea69bb00330de1037a48ac13dbc5b72f346c801d97731dec8260f400c +HadGEM2-CC_360day/tasmin_day_HadGEM2-CC_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc sha256:5c8fa666603fd68f614d95ac8c5a0dbdfb9f8e2e86666a270516a38526c1aa20 +NRCANdaily/nrcan_canada_daily_pr_1990.nc sha256:144479ec7a976cfecb6a10762d128a771356093d72caf5f075508ee86d25a1b0 +NRCANdaily/nrcan_canada_daily_tasmax_1990.nc sha256:84880205b798740e37a102c7f40e595d7a4fde6e35fb737a1ef68b8dad447526 +NRCANdaily/nrcan_canada_daily_tasmin_1990.nc sha256:13d61fc54cdcb4c1617ec777ccbf59575d8fdc24754f914042301bc1b024d7f7 +Raven/q_sim.nc sha256:f7a0ae73c498235e1c3e7338a184c5ca3729941b81521e606aa60b2c639f6e71 +SpatialAnalogs/CanESM2_ScenGen_Chibougamau_2041-2070.nc sha256:b6cfc4a963d68b6da8978acd26ffb506f33c9c264d8057badd90bf47cd9f3f3d +SpatialAnalogs/NRCAN_SECan_1981-2010.nc sha256:bde680ddad84106caad3a2e83a70ecdd8138578a70e875d77c2ec6d3ff868fee +SpatialAnalogs/dissimilarity.nc sha256:200ab9b7d43d41e6db917c54d35b43e3c5853e0df701e44efd5b813e47590110 +SpatialAnalogs/indicators.nc sha256:3bcbb0e4540d4badc085ac42b9d04a353e815fb55c62271eb73275b889c80a15 cmip3/tas.sresb1.giss_model_e_r.run1.atm.da.nc sha256:e709552beeeccafcfe280759edf5477ae5241c698409ca051b0899c16e92c95e cmip5/tas_Amon_CanESM2_rcp85_r1i1p1_200701-200712.nc sha256:7471770e4e654997225ab158f2b24aa0510b6f06006fb757b9ea7c0d4a47e1f2 cmip5/tas_Amon_HadGEM2-ES_rcp85_r1i1p1_200512-203011.nc sha256:3cb54d67bf89cdf542a7b93205785da3800f9a77eaa8436f4ee74af13b248b95 @@ -17,25 +39,9 @@ cmip5/tas_Amon_HadGEM2-ES_rcp85_r1i1p1_224912-227411.nc sha256:abbe16349870c5013 cmip5/tas_Amon_HadGEM2-ES_rcp85_r1i1p1_227412-229911.nc sha256:ecf52dc8ac13e04d0b643fc53cc5b367b32e68a311e6718686eaa87088788f98 cmip5/tas_Amon_HadGEM2-ES_rcp85_r1i1p1_229912-229912.nc sha256:3fa657483072d8a04363b8718bc9c4e63e6354617a4ab3d627b25222a4cd094c cmip6/o3_Amon_GFDL-ESM4_historical_r1i1p1f1_gr1_185001-194912.nc sha256:cfff189d4986289efb2b88f418cd6d65b26b59355b67b73ca26ac8fa12a9f83f +cmip6/prsn_day_CanESM5_historical_r1i1p1f1_gn_19910101-20101231.nc sha256:b272ae29fd668cd8a63ed2dc7949a1fd380ec67da98561a4beb34da371439815 cmip6/sic_SImon_CCCma-CanESM5_ssp245_r13i1p2f1_2020.nc sha256:58a03aa401f80751ad60c8950f14bcf717aeb6ef289169cb5ae3081bb4689825 -CRCM5/tasmax_bby_198406_se.nc sha256:9a80cc19ed212428ef90ce0cc40790fbf0d1fc301df0abdf578da45843dae93d -EnsembleReduce/TestEnsReduceCriteria.nc sha256:ae7a70b9d5c54ab072f1cfbfab91d430a41c5067db3c1968af57ea2122cfe8e7 -EnsembleStats/BCCAQv2+ANUSPLIN300_ACCESS1-0_historical+rcp45_r1i1p1_1950-2100_tg_mean_YS.nc sha256:ca0cc893cf91db7c6dfe3df10d605684eabbea55b7e26077c10142d302e55aed -EnsembleStats/BCCAQv2+ANUSPLIN300_BNU-ESM_historical+rcp45_r1i1p1_1950-2100_tg_mean_YS.nc sha256:c796276f563849c31bf388a3beb4a440eeb72062a84b4cf9760c854d1e990ca4 -EnsembleStats/BCCAQv2+ANUSPLIN300_CCSM4_historical+rcp45_r1i1p1_1950-2100_tg_mean_YS.nc sha256:9cfa9bc4e81e936eb680a55db428ccd9f0a6d366d4ae2c4a9064bfa5d71e5ca7 -EnsembleStats/BCCAQv2+ANUSPLIN300_CCSM4_historical+rcp45_r2i1p1_1950-2100_tg_mean_YS.nc sha256:ca36aafb3c63ddb6bfc8537abb854b71f719505c1145d5c81c3315eb1a13647c -EnsembleStats/BCCAQv2+ANUSPLIN300_CNRM-CM5_historical+rcp45_r1i1p1_1970-2050_tg_mean_YS.nc sha256:623eab96d75d8cc8abd59dfba1c14cfb06fd7c0fe9ce86788d3c8b0891684df2 -ERA5/daily_surface_cancities_1990-1993.nc sha256:049d54ace3d229a96cc621189daa3e1a393959ab8d988221cfc7b2acd7ab94b2 -FWI/GFWED_sample_2017.nc sha256:cf3bde795825663894fa7619a028d5a14fee307c623968235f25393f7afe159e -FWI/cffdrs_test_fwi.nc sha256:147be24e080aa67f17261f61f05a5dfb381a66a23785a327e47e2303667ca3ab -FWI/cffdrs_test_wDC.nc sha256:ebadcad1dd6a1a1e93c29a1143d7caefd46593ea2fbeb721015245981cce90c3 -HadGEM2-CC_360day/pr_day_HadGEM2-CC_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc sha256:c45ff4c17ba9fd92392bb08a7705789071a0bec40bde48f5a838ff12413cc33b -HadGEM2-CC_360day/tasmax_day_HadGEM2-CC_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc sha256:aa3eb54ea69bb00330de1037a48ac13dbc5b72f346c801d97731dec8260f400c -HadGEM2-CC_360day/tasmin_day_HadGEM2-CC_rcp85_r1i1p1_na10kgrid_qm-moving-50bins-detrend_2095.nc sha256:5c8fa666603fd68f614d95ac8c5a0dbdfb9f8e2e86666a270516a38526c1aa20 -NRCANdaily/nrcan_canada_daily_pr_1990.nc sha256:144479ec7a976cfecb6a10762d128a771356093d72caf5f075508ee86d25a1b0 -NRCANdaily/nrcan_canada_daily_tasmax_1990.nc sha256:84880205b798740e37a102c7f40e595d7a4fde6e35fb737a1ef68b8dad447526 -NRCANdaily/nrcan_canada_daily_tasmin_1990.nc sha256:13d61fc54cdcb4c1617ec777ccbf59575d8fdc24754f914042301bc1b024d7f7 -Raven/q_sim.nc sha256:f7a0ae73c498235e1c3e7338a184c5ca3729941b81521e606aa60b2c639f6e71 +cmip6/snw_day_CanESM5_historical_r1i1p1f1_gn_19910101-20101231.nc sha256:05263d68f5c7325439a170990731fcb90d1103a6c5e4f0c0fd1d3a44b92e88e0 sdba/CanESM2_1950-2100.nc sha256:b41fe603676e70d16c747ec207eb75ec86a39b665de401dcb23b5969ab3e1b32 sdba/adjusted_external.nc sha256:ff325c88eca96844bc85863744e4e08bcdf3d257388255636427ad5e11960d2e sdba/ahccd_1950-2013.nc sha256:7e9a1f61c1d04ca257b09857a82715f1fa3f0550d77f97b7306d4eaaf0c70239 @@ -44,8 +50,4 @@ uncertainty_partitioning/cmip5_pr_global_mon.nc sha256:7e585c995e95861979fd23dd9 uncertainty_partitioning/cmip5_pr_pnw_mon.nc sha256:1cdfe74f5bd5cf71cd0737c190277821ea90e4e79de5b37367bf2b82c35a66c9 uncertainty_partitioning/cmip5_tas_global_mon.nc sha256:41ba79a43bab169a0487e3f3f66a68a699bef9355a13e26a87fdb65744555cb5 uncertainty_partitioning/cmip5_tas_pnw_mon.nc sha256:eeb48765fd430186f3634e7f779b4be45ab3df73e806a4cbb743fefb13279398 -SpatialAnalogs/CanESM2_ScenGen_Chibougamau_2041-2070.nc sha256:b6cfc4a963d68b6da8978acd26ffb506f33c9c264d8057badd90bf47cd9f3f3d -SpatialAnalogs/NRCAN_SECan_1981-2010.nc sha256:bde680ddad84106caad3a2e83a70ecdd8138578a70e875d77c2ec6d3ff868fee -SpatialAnalogs/dissimilarity.nc sha256:200ab9b7d43d41e6db917c54d35b43e3c5853e0df701e44efd5b813e47590110 -SpatialAnalogs/indicators.nc sha256:3bcbb0e4540d4badc085ac42b9d04a353e815fb55c62271eb73275b889c80a15 uncertainty_partitioning/seattle_avg_tas.csv sha256:157d6721f9925eec8268848e34548df2b1da50935f247a9b136d251ef53898d7 diff --git a/src/xclim/testing/utils.py b/src/xclim/testing/utils.py index 467fc6493..a06add28d 100644 --- a/src/xclim/testing/utils.py +++ b/src/xclim/testing/utils.py @@ -72,7 +72,7 @@ "testing_setup_warnings", ] -default_testdata_version = "v2024.8.23" +default_testdata_version = "v2025.1.8" """Default version of the testing data to use when fetching datasets.""" default_testdata_repo_url = ( diff --git a/tests/test_indices.py b/tests/test_indices.py index 932909198..f766b7d7f 100644 --- a/tests/test_indices.py +++ b/tests/test_indices.py @@ -28,6 +28,7 @@ from xclim.core.calendar import percentile_doy from xclim.core.options import set_options from xclim.core.units import convert_units_to, units +from xclim.indices import prsnd_to_prsn K2C = 273.15 @@ -1471,7 +1472,7 @@ def test_1d( class TestHolidayIndices: def test_xmas_days_simple(self, snd_series): - # 5ish years of data + # 5ish years of data, starting from 2000-07-01 snd = snd_series(np.zeros(365 * 5), units="cm") # add snow on ground on December 25 for first 3 years @@ -1482,10 +1483,10 @@ def test_xmas_days_simple(self, snd_series): snd.loc["2004-12-25"] = 6 out = xci.holiday_snow_days(snd) - np.testing.assert_array_equal(out, [1, 0, 1, 0, 1]) + np.testing.assert_array_equal(out, [1, 0, 1, 0, 1, 0]) def test_xmas_days_range(self, snd_series): - # 5ish years of data + # 5ish years of data, starting from 2000-07-01 snd = snd_series(np.zeros(365 * 5), units="cm") # add snow on ground on December 25 for first 3 years @@ -1497,13 +1498,14 @@ def test_xmas_days_range(self, snd_series): snd.loc["2004-12-25"] = 6 out = xci.holiday_snow_days(snd, date_start="12-24", date_end="12-25") - np.testing.assert_array_equal(out, [1, 0, 2, 0, 1]) + np.testing.assert_array_equal(out, [1, 0, 2, 0, 1, 0]) - def test_perfect_xmas_days_simple(self, snd_series, prsn_series): - # 5ish years of data + def test_perfect_xmas_days(self, snd_series, prsn_series): + # 5ish years of data, starting from 2000-07-01 a = np.zeros(365 * 5) snd = snd_series(a, units="mm") - prsn = prsn_series(a.copy(), units="cm day-1") + # prsnd is snowfall using snow density of 100 kg/m3 + prsnd = prsn_series(a.copy(), units="cm day-1") # add snow on ground on December 25 snd.loc["2000-12-25"] = 20 @@ -1514,16 +1516,33 @@ def test_perfect_xmas_days_simple(self, snd_series, prsn_series): snd.loc["2004-12-25"] = 60 # add snowfall on December 25 - prsn.loc["2000-12-25"] = 5 - prsn.loc["2001-12-25"] = 2 - prsn.loc["2001-12-26"] = 30 # too bad it's Boxing Day - prsn.loc["2002-12-25"] = 0.5 # not quite enough - prsn.loc["2003-12-25"] = 0 # no snow - prsn.loc["2004-12-25"] = 10 + prsnd.loc["2000-12-25"] = 5 + prsnd.loc["2001-12-25"] = 2 + prsnd.loc["2001-12-26"] = 30 # too bad it's Boxing Day + prsnd.loc["2002-12-25"] = 1 # not quite enough + prsnd.loc["2003-12-25"] = 0 # no snow + prsnd.loc["2004-12-25"] = 10 + + prsn = prsnd_to_prsn(prsnd) prsn = convert_units_to(prsn, "kg m-2 s-1", context="hydro") - out = xci.holiday_snow_and_snowfall_days(snd, prsn) - np.testing.assert_array_equal(out, [1, 0, 0, 0, 1]) + out1 = xci.holiday_snow_and_snowfall_days(snd, prsn) + np.testing.assert_array_equal(out1, [1, 0, 0, 0, 1]) + + out2 = xci.holiday_snow_and_snowfall_days( + snd, prsn, snd_thresh="15 mm", prsn_thresh="0.5 mm" + ) + np.testing.assert_array_equal(out2, [1, 1, 1, 0, 1]) + + out3 = xci.holiday_snow_and_snowfall_days( + snd, + prsn, + snd_thresh="10 mm", + prsn_thresh="0.5 mm", + date_start="12-25", + date_end="12-26", + ) + np.testing.assert_array_equal(out3, [1, 2, 1, 0, 1]) class TestHotSpellFrequency: diff --git a/tests/test_snow.py b/tests/test_snow.py index 7158c58d8..395faadbc 100644 --- a/tests/test_snow.py +++ b/tests/test_snow.py @@ -171,11 +171,11 @@ def test_perfect_xmas_days_simple(self, nimbus): np.testing.assert_array_equal( out.sum(dim="time"), [ - [0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.0, 0.0, 1.0, 0.0], - [1.0, 0.0, 0.0, 0.0, 1.0], - [0.0, 0.0, 1.0, 1.0, 1.0], - [0.0, 0.0, 0.0, 0.0, 0.0], + [3.0, 0.0, 0.0, 0.0, 0.0], + [5.0, 2.0, 1.0, 1.0, 1.0], + [6.0, 5.0, 4.0, 4.0, 5.0], + [7.0, 11.0, 12.0, 9.0, 6.0], + [10.0, 8.0, 12.0, 10.0, 8.0], + [9.0, 11.0, 10.0, 7.0, 9.0], ], ) From a18d6624a87e513f1d6d975dc79a21d462bc6ccb Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:44:42 -0500 Subject: [PATCH 15/18] synchronize some dependencies Signed-off-by: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> --- environment.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/environment.yml b/environment.yml index cd02063aa..d30b4791a 100644 --- a/environment.yml +++ b/environment.yml @@ -29,14 +29,14 @@ dependencies: - lmoments3 >=1.0.7 # Required for some Jupyter notebooks - pot >=0.9.4 # Testing and development dependencies - - black ==24.10.0 + - black =24.10.0 - blackdoc ==0.3.9 - bump-my-version >=0.28.1 - cairosvg >=2.6.0 - - codespell ==2.3.0 + - codespell =2.3.0 - coverage >=7.5.0 - coveralls >=4.0.1 # Note: coveralls is not yet compatible with Python 3.13 - - deptry ==0.20.0 + - deptry =0.21.2 - distributed >=2.0 - flake8 >=7.1.1 - flake8-rst-docstrings >=0.3.0 @@ -45,7 +45,7 @@ dependencies: - h5netcdf >=1.3.0 - ipykernel - ipython >=8.5.0 - - isort ==5.13.2 + - isort =5.13.2 - matplotlib >=3.6.0 - mypy >=1.10.0 - nbconvert <7.14 # Pinned due to directive errors in sphinx. See: https://github.com/jupyter/nbconvert/issues/2092 @@ -77,7 +77,7 @@ dependencies: - tokenize-rt >=5.2.0 - tox >=4.21.2 - tox-gh >=1.4.4 - - vulture ==2.13 + - vulture ==2.14 - xdoctest >=1.1.5 - yamllint >=1.35.1 - pip >=24.2.0 From 876a1fb994f624674eede0bb4c0063e1aa6433fe Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:10:22 -0500 Subject: [PATCH 16/18] add workaround for pygments breaking changes with sphinx-codeautolink, update nbconvert Signed-off-by: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> --- environment.yml | 7 ++++--- pyproject.toml | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/environment.yml b/environment.yml index d30b4791a..e00b3d954 100644 --- a/environment.yml +++ b/environment.yml @@ -30,7 +30,7 @@ dependencies: - pot >=0.9.4 # Testing and development dependencies - black =24.10.0 - - blackdoc ==0.3.9 + - blackdoc =0.3.9 - bump-my-version >=0.28.1 - cairosvg >=2.6.0 - codespell =2.3.0 @@ -48,7 +48,7 @@ dependencies: - isort =5.13.2 - matplotlib >=3.6.0 - mypy >=1.10.0 - - nbconvert <7.14 # Pinned due to directive errors in sphinx. See: https://github.com/jupyter/nbconvert/issues/2092 + - nbconvert >=7.16.4 - nbqa >=1.8.2 - nbsphinx >=0.9.5 - nbval >=0.11.0 @@ -60,6 +60,7 @@ dependencies: - pooch >=1.8.0 - pre-commit >=3.7 - pybtex >=0.24.0 + - pygments <2.19 # FIXME: temporary fix for sphinx-codeautolink - pylint >=3.3.1 - pytest >=8.0.0 - pytest-cov >=5.0.0 @@ -77,7 +78,7 @@ dependencies: - tokenize-rt >=5.2.0 - tox >=4.21.2 - tox-gh >=1.4.4 - - vulture ==2.14 + - vulture =2.14 - xdoctest >=1.1.5 - yamllint >=1.35.1 - pip >=24.2.0 diff --git a/pyproject.toml b/pyproject.toml index f665e2cde..34b7ea2a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ dev = [ "ipython >=8.5.0", "isort ==5.13.2", "mypy >=1.10.0", - "nbconvert <7.14", # Pinned due to directive errors in sphinx. See: https://github.com/jupyter/nbconvert/issues/2092 + "nbconvert >=7.16.4", "nbqa >=1.8.2", "nbval >=0.11.0", "numpydoc >=1.8.0", @@ -104,6 +104,7 @@ docs = [ "nc-time-axis >=1.4.1", "pooch >=1.8.0", "pybtex >=0.24.0", + "pygments <2.19", # FIXME: temporary fix for sphinx-codeautolink "sphinx >=7.0.0", "sphinx-autobuild >=2024.4.16", "sphinx-autodoc-typehints", From 9eaa38821c84c64905c62f0166bce3133716e2c9 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:19:29 -0500 Subject: [PATCH 17/18] more sphinx-codeautolink workarounds Signed-off-by: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> --- environment.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index e00b3d954..b22c97c84 100644 --- a/environment.yml +++ b/environment.yml @@ -70,7 +70,7 @@ dependencies: - sphinx >=7.0.0 - sphinx-autobuild >=2024.4.16 - sphinx-autodoc-typehints - - sphinx-codeautolink + - sphinx-codeautolink >=0.15.2,!=0.16.0 # FIXME: temporary fix for sphinx-codeautolink - sphinx-copybutton - sphinx-mdinclude - sphinxcontrib-bibtex diff --git a/pyproject.toml b/pyproject.toml index 34b7ea2a2..80488e236 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,7 +108,7 @@ docs = [ "sphinx >=7.0.0", "sphinx-autobuild >=2024.4.16", "sphinx-autodoc-typehints", - "sphinx-codeautolink", + "sphinx-codeautolink >=0.15.2,!=0.16.0", # FIXME: temporary fix for sphinx-codeautolink "sphinx-copybutton", "sphinx-mdinclude", "sphinxcontrib-bibtex", From 4d2ef2b8333cb498cb9d5aa47df57699d06ec9d2 Mon Sep 17 00:00:00 2001 From: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:34:44 -0500 Subject: [PATCH 18/18] update CHANGELOG.rst Signed-off-by: Trevor James Smith <10819524+Zeitsperre@users.noreply.github.com> --- CHANGELOG.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7a66fc4db..e846cb30f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,14 @@ New indicators * Added ``xclim.indices.holiday_snow_days`` to compute the number of days with snow on the ground during holidays ("Christmas Days"). (:issue:`2029`, :pull:`2030`). * Added ``xclim.indices.holiday_snow_and_snowfall_days`` to compute the number of days with snow on the ground and measurable snowfall during holidays ("Perfect Christmas Days"). (:issue:`2029`, :pull:`2030`). +New features and enhancements +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* Added a new ``xclim.indices.generic.bivariate_count_occurrences`` function to count instances where operations and performed and validated for two variables. (:pull:`2030`). + +Internal changes +^^^^^^^^^^^^^^^^ +* `sphinx-codeautolink` and `pygments` have been temporarily pinned due to breaking API changes. (:pull:`2030`). + v0.54.0 (2024-12-16) -------------------- Contributors to this version: Trevor James Smith (:user:`Zeitsperre`), Pascal Bourgault (:user:`aulemahal`), Éric Dupuis (:user:`coxipi`), Sascha Hofmann (:user:`saschahofmann`).