diff --git a/CHANGES.rst b/CHANGES.rst index 7dea66409..b89eb7d07 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,6 +13,7 @@ Announcements New indicators ^^^^^^^^^^^^^^ * New ``snw_season_length`` and ``snd_season_length`` computing the duration between the start and the end of the snow season, both defined as the first day of a continuous period with snow above/under a threshold. Previous versions of these indicators were renamed ``snw_days_above`` and ``snd_days_above`` to better reflect what they computed : the number of days with snow above a given threshold (with no notion of continuity). (:issue:`1703`, :pull:`1708`). +* Added ``atmos.duff_moisture_code``, part of the Canadian Forest Fire Weather Index System. It was already an output of the `atmos.cffwis_indices`, but now has its own standalone indicator. (:issue:`1698`, :pull:`1712`). Breaking changes ^^^^^^^^^^^^^^^^ diff --git a/tests/test_cffwis.py b/tests/test_cffwis.py index f9f3a3cb4..d17ae4b8e 100644 --- a/tests/test_cffwis.py +++ b/tests/test_cffwis.py @@ -458,3 +458,47 @@ def test_gfwed_and_indicators(self, open_dataset): [ds2.DC, ds2.DMC, ds2.FFMC, ds2.ISI, ds2.BUI, ds2.FWI], outs ): np.testing.assert_allclose(out, exp, rtol=0.03) + + def test_gfwed_drought_code(self, open_dataset): + # Also tests passing parameters as quantity strings + ds = open_dataset("FWI/GFWED_sample_2017.nc") + + out = atmos.drought_code( + tas=ds.tas, + pr=ds.prbc, + snd=ds.snow_depth, + lat=ds.lat, + season_method="GFWED", + overwintering=False, + dry_start="GFWED", + temp_condition_days=3, + snow_condition_days=3, + temp_start_thresh="6 degC", + temp_end_thresh="6 degC", + ) + + np.testing.assert_allclose( + out.isel(loc=[0, 1]), ds.DC.isel(loc=[0, 1]), rtol=0.03 + ) + + def test_gfwed_duff_moisture_code(self, open_dataset): + # Also tests passing parameters as quantity strings + ds = open_dataset("FWI/GFWED_sample_2017.nc") + + out = atmos.duff_moisture_code( + tas=ds.tas, + pr=ds.prbc, + hurs=ds.rh, + snd=ds.snow_depth, + lat=ds.lat, + season_method="GFWED", + dry_start="GFWED", + temp_condition_days=3, + snow_condition_days=3, + temp_start_thresh="6 degC", + temp_end_thresh="6 degC", + ) + + np.testing.assert_allclose( + out.isel(loc=[0, 1]), ds.DMC.isel(loc=[0, 1]), rtol=0.03 + ) diff --git a/xclim/data/fr.json b/xclim/data/fr.json index da193349f..be0cc4a14 100644 --- a/xclim/data/fr.json +++ b/xclim/data/fr.json @@ -799,6 +799,12 @@ "long_name": "Indice forêt météo", "description": "Évaluation numérique de l'intensité du feu." }, + "DMC": { + "long_name": "Indice de l'humus", + "description": "Évaluation numérique de la teneur moyenne en eau des couches organiques peu tassées de moyenne épaisseur.", + "title": "Indice de l'humus (IH)", + "abstract": "Évaluation numérique de la teneur moyenne en eau des couches organiques peu tassées de moyenne épaisseur." + }, "WIND_POWER_POTENTIAL": { "long_name": "Potentiel de production éolienne", "description": "Potentiel de production éolienne estimé par une courbe de puissance semi-idéalisée d'une turbine, paramétrée par une vitesse minimale de démarrage {cut_in}, la vitesse nominale {rated}, et la vitesse d'arrêt {cut_out}.", diff --git a/xclim/indicators/atmos/_precip.py b/xclim/indicators/atmos/_precip.py index f6b906b76..a47658e4e 100644 --- a/xclim/indicators/atmos/_precip.py +++ b/xclim/indicators/atmos/_precip.py @@ -29,6 +29,7 @@ "dry_spell_max_length", "dry_spell_total_length", "dryness_index", + "duff_moisture_code", "first_snowfall", "fraction_over_precip_doy_thresh", "fraction_over_precip_thresh", @@ -394,7 +395,6 @@ class HrPrecip(Hourly): title="Daily drought code", identifier="dc", units="", - standard_name="drought_code", long_name="Drought Code", description="Numerical code estimating the average moisture content of organic layers.", abstract="The Drought Index is part of the Canadian Forest-Weather Index system. " @@ -403,6 +403,14 @@ class HrPrecip(Hourly): missing="skip", ) +duff_moisture_code = FireWeather( + identifier="dmc", + units="", + long_name="Duff Moisture Code", + description="Numeric rating of the average moisture content of loosely compacted organic layers of moderate depth.", + compute=indices.duff_moisture_code, + missing="skip", +) cffwis_indices = FireWeather( identifier="cffwis", diff --git a/xclim/indices/__init__.py b/xclim/indices/__init__.py index 9689d5208..b7acd4544 100644 --- a/xclim/indices/__init__.py +++ b/xclim/indices/__init__.py @@ -13,6 +13,7 @@ from .fire import ( cffwis_indices, drought_code, + duff_moisture_code, fire_season, griffiths_drought_factor, keetch_byram_drought_index, diff --git a/xclim/indices/fire/_cffwis.py b/xclim/indices/fire/_cffwis.py index c0201e04a..8012e7ebc 100644 --- a/xclim/indices/fire/_cffwis.py +++ b/xclim/indices/fire/_cffwis.py @@ -150,6 +150,7 @@ "cffwis_indices", "daily_severity_rating", "drought_code", + "duff_moisture_code", "fire_season", "fire_weather_index", "fire_weather_ufunc", @@ -1247,12 +1248,13 @@ def overwintering_drought_code( return wDC -# TODO: The `noqa` can be removed when this function is no longer public -def _convert_parameters(params: dict[str, int | float]) -> dict[str, int | float]: +def _convert_parameters( + params: dict[str, int | float], funcname: str = "fire weather indices" +) -> dict[str, int | float]: for param, value in params.copy().items(): if param not in default_params: raise ValueError( - f"{param} is not a valid parameter for fire weather indices. See list in xc.indices.fire.default_params." + f"{param} is not a valid parameter for {funcname}. See the docstring of the function and the list in xc.indices.fire.default_params." ) if isinstance(default_params[param], tuple): params[param] = convert_units_to(value, default_params[param][1]) @@ -1469,12 +1471,107 @@ def drought_code( overwintering=overwintering, dry_start=dry_start, initial_start_up=initial_start_up, - **_convert_parameters(params), + **_convert_parameters(params, "drought_code"), ) out["DC"].attrs["units"] = "" return out["DC"] +@declare_units( + tas="[temperature]", + pr="[precipitation]", + hurs="[]", + lat="[]", + snd="[length]", + dmc0="[]", + season_mask="[]", +) +def duff_moisture_code( + tas: xr.DataArray, + pr: xr.DataArray, + hurs: xr.DataArray, + lat: xr.DataArray, + snd: xr.DataArray | None = None, + dmc0: xr.DataArray | None = None, + season_mask: xr.DataArray | None = None, + season_method: str | None = None, + dry_start: str | None = None, + initial_start_up: bool = True, + **params, +) -> xr.DataArray: + r"""Duff moisture code (FWI component). + + The duff moisture code is part of the Canadian Forest Fire Weather Index System. + It is a numeric rating of the average moisture content of loosely compacted organic layers of moderate depth. + + Parameters + ---------- + tas : xr.DataArray + Noon temperature. + pr : xr.DataArray + Rain fall in open over previous 24 hours, at noon. + hurs : xr.DataArray + Noon relative humidity. + lat : xr.DataArray + Latitude coordinate + snd : xr.DataArray + Noon snow depth. + dmc0 : xr.DataArray + Initial values of the duff moisture code. + season_mask : xr.DataArray, optional + Boolean mask, True where/when the fire season is active. + season_method : {None, "WF93", "LA08", "GFWED"} + How to compute the start-up and shutdown of the fire season. + If "None", no start-ups or shutdowns are computed, similar to the R fire function. + Ignored if `season_mask` is given. + dry_start : {None, "CFS", 'GFWED'} + Whether to activate the DC and DMC "dry start" mechanism and which method to use. + See :py:func:`fire_weather_ufunc`. + initial_start_up : bool + If True (default), grid points where the fire season is active on the first timestep go through a start_up phase + for that time step. Otherwise, previous codes must be given as a continuing fire season is assumed for those + points. + params + Any other keyword parameters as defined in `xclim.indices.fire.fire_weather_ufunc` and in :py:data:`default_params`. + + Returns + ------- + xr.DataArray, [dimensionless] + Duff moisture code + + Notes + ----- + See :cite:cts:`code-natural_resources_canada_data_nodate`, the :py:mod:`xclim.indices.fire` module documentation, + and the docstring of :py:func:`fire_weather_ufunc` for more information. This algorithm follows the official R code + released by the CFS, which contains revisions from the original 1982 Fortran code. + + References + ---------- + :cite:cts:`fire-wang_updated_2015` + """ + tas = convert_units_to(tas, "C") + pr = convert_units_to(pr, "mm/day") + if snd is not None: + snd = convert_units_to(snd, "m") + + out = fire_weather_ufunc( + tas=tas, + pr=pr, + hurs=hurs, + lat=lat, + dmc0=dmc0, + snd=snd, + indexes=["DMC"], + season_mask=season_mask, + season_method=season_method, + dry_start=dry_start, + initial_start_up=initial_start_up, + **_convert_parameters(params, "duff_moisture_code"), + ) + out["DMC"].attrs["units"] = "" + return out["DMC"] + + @declare_units( tas="[temperature]", snd="[length]",