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",