Repository: Ouranosinc/xclim

Issue:
Harmonize freeze-thaw cycle computation with PCC
PCC is using thresholds different than 0. 

Discussion:
coveralls: ## Pull Request Test Coverage Report for [Build 1069](https://coveralls.io/builds/25607282)

* **6** of **6**   **(100.0%)**  changed or added relevant lines in **4** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **92.521%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/25607282/badge)](https://coveralls.io/builds/25607282) |
| :-- | --: |
| Change from base [Build 1058](https://coveralls.io/builds/25372759): |  0.0% |
| Covered Lines: | 1336 |
| Relevant Lines: | 1444 |

---
##### 💛  - [Coveralls](https://coveralls.io)

tlogan2000: FYI - PCC has indivudual tmin and tmax thresholds ...

Patch:
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index ac3a75cd..4be835ef 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -2,7 +2,7 @@
 * Python version:
 * Operating System:
 
-### Description 
+### Description
 <!--Describe what you were trying to get done.
 Tell us what happened, what went wrong, and what you expected to happen.-->
 
diff --git a/docs/index.rst b/docs/index.rst
index 9ce43858..45822ea0 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -22,4 +22,3 @@ Indices and tables
 * :ref:`genindex`
 * :ref:`modindex`
 * :ref:`search`
-
diff --git a/docs/itable.py b/docs/itable.py
index a8331230..b664f67e 100644
--- a/docs/itable.py
+++ b/docs/itable.py
@@ -19,13 +19,17 @@ def _get_indicators(modules):
 def _indicator_table():
     """Return a sequence of dicts storing metadata about all available indices."""
     from xclim import atmos
-    import inspect2
+    import inspect
 
     inds = _get_indicators([atmos])
     table = []
     for ind in inds:
         # Apply default values
-        args = {name: p.default for (name, p) in ind._sig.parameters.items() if p.default != inspect2._empty}
+        args = {
+            name: p.default
+            for (name, p) in ind._sig.parameters.items()
+            if p.default != inspect._empty
+        }
         table.append(ind.json(args))
     return table
 
diff --git a/docs/itable.rst b/docs/itable.rst
index 632d104a..6f0270dc 100644
--- a/docs/itable.rst
+++ b/docs/itable.rst
@@ -33,6 +33,3 @@ Table of indicators
       background-color: #dddddd;
     }
    </style>
-
-
-
diff --git a/docs/rstjinja.py b/docs/rstjinja.py
index 1b7725b1..521a6009 100644
--- a/docs/rstjinja.py
+++ b/docs/rstjinja.py
@@ -3,14 +3,12 @@ def rstjinja(app, docname, source):
     Render our pages as a jinja template for fancy templating goodness.
     """
     # Make sure we're outputting HTML
-    if app.builder.format != 'html':
+    if app.builder.format != "html":
         return
     src = source[0]
-    rendered = app.builder.templates.render_string(
-        src, app.config.html_context
-    )
+    rendered = app.builder.templates.render_string(src, app.config.html_context)
     source[0] = rendered
-    
+
 
 def setup(app):
     app.connect("source-read", rstjinja)
diff --git a/setup.cfg b/setup.cfg
index 754e5414..a5a9975c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -41,6 +41,7 @@ ignore =
 	F401
 	F403
 	W503
+    W504
 
 [aliases]
 test = pytest
diff --git a/xclim/atmos/__init__.py b/xclim/atmos/__init__.py
index 583a091f..6edcff11 100644
--- a/xclim/atmos/__init__.py
+++ b/xclim/atmos/__init__.py
@@ -10,5 +10,5 @@ The concept followed here is to define Indicator subclasses for each input varia
 for each indicator.
 
 """
-from ._temperature import *
 from ._precip import *
+from ._temperature import *
diff --git a/xclim/atmos/_temperature.py b/xclim/atmos/_temperature.py
index d13e8894..539c6293 100644
--- a/xclim/atmos/_temperature.py
+++ b/xclim/atmos/_temperature.py
@@ -306,7 +306,7 @@ daily_freezethaw_cycles = TasminTasmax(
     standard_name="daily_freezethaw_cycles",
     long_name="daily freezethaw cycles",
     description="{freq} number of days with a diurnal freeze-thaw cycle "
-    ": Tmax > 0℃ and Tmin < 0℃.",
+    ": Tmax > {thresh_tasmax} and Tmin <= {thresh_tasmin}.",
     cell_methods="",
     compute=indices.daily_freezethaw_cycles,
 )
diff --git a/xclim/indices/__init__.py b/xclim/indices/__init__.py
index 7905c777..96463f9f 100644
--- a/xclim/indices/__init__.py
+++ b/xclim/indices/__init__.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-
 """
 Indices library
 ===============
@@ -68,9 +67,9 @@ Indice descriptions
 .. _`NumPy`: https://numpydoc.readthedocs.io/en/latest/format.html#docstring-standard
 """
 
+from ._multivariate import *
 from ._simple import *
 from ._threshold import *
-from ._multivariate import *
 
 # TODO: Define a unit conversion system for temperature [K, C, F] and precipitation [mm h-1, Kg m-2 s-1] metrics
 # TODO: Move utility functions to another file.
diff --git a/xclim/indices/_multivariate.py b/xclim/indices/_multivariate.py
index ef34fe6c..969f59eb 100644
--- a/xclim/indices/_multivariate.py
+++ b/xclim/indices/_multivariate.py
@@ -1,4 +1,5 @@
 import logging
+import warnings
 
 import numpy as np
 import xarray as xr
@@ -153,11 +154,23 @@ def cold_and_dry_days(tas, tgin25, pr, wet25, freq="YS"):
     # return c.resample(time=freq).sum(dim='time')
 
 
-@declare_units("days", tasmax="[temperature]", tasmin="[temperature]")
-def daily_freezethaw_cycles(tasmax, tasmin, freq="YS"):
+@declare_units(
+    "days",
+    tasmax="[temperature]",
+    tasmin="[temperature]",
+    thresh_tasmax="[temperature]",
+    thresh_tasmin="[temperature]",
+)
+def daily_freezethaw_cycles(
+    tasmax,
+    tasmin,
+    thresh_tasmax="UNSET 0 degC",
+    thresh_tasmin="UNSET 0 degC",
+    freq="YS",
+):
     r"""Number of days with a diurnal freeze-thaw cycle
 
-    The number of days where Tmax > 0℃ and Tmin < 0℃.
+    The number of days where Tmax > thresh_tasmax and Tmin <= thresh_tasmin.
 
     Parameters
     ----------
@@ -165,6 +178,10 @@ def daily_freezethaw_cycles(tasmax, tasmin, freq="YS"):
       Maximum daily temperature [℃] or [K]
     tasmin : xarray.DataArray
       Minimum daily temperature values [℃] or [K]
+    thresh_tasmax : str
+      The temperature threshold needed to trigger a thaw event [℃] or [K]. Default : '0 degC'
+    thresh_tasmin : str
+      The temperature threshold needed to trigger a freeze event [℃] or [K]. Default : '0 degC'
     freq : str
       Resampling frequency
 
@@ -185,9 +202,18 @@ def daily_freezethaw_cycles(tasmax, tasmin, freq="YS"):
 
     where :math:`[P]` is 1 if :math:`P` is true, and 0 if false.
     """
-    frz = utils.convert_units_to("0 degC", tasmax)
-    ft = (tasmin < frz) * (tasmax > frz) * 1
+    if thresh_tasmax.startswith("UNSET ") or thresh_tasmin.startswith("UNSET"):
+        thresh_tasmax, thresh_tasmin = (
+            thresh_tasmax.replace("UNSET ", ""),
+            thresh_tasmin.replace("UNSET ", ""),
+        )
+
+    thaw_threshold = utils.convert_units_to(thresh_tasmax, tasmax)
+    freeze_threshold = utils.convert_units_to(thresh_tasmin, tasmin)
+
+    ft = (tasmin <= freeze_threshold) * (tasmax > thaw_threshold) * 1
     out = ft.resample(time=freq).sum(dim="time")
+
     return out
 
 
diff --git a/xclim/indices/_threshold.py b/xclim/indices/_threshold.py
index 373af4a3..e93a7e57 100644
--- a/xclim/indices/_threshold.py
+++ b/xclim/indices/_threshold.py
@@ -40,7 +40,7 @@ __all__ = [
     "tropical_nights",
 ]
 
-
+# TODO: Add docstring for window
 @declare_units("days", tas="[temperature]", thresh="[temperature]")
 def cold_spell_days(tas, thresh="-10 degC", window=5, freq="AS-JUL"):
     r"""Cold spell days
diff --git a/xclim/utils.py b/xclim/utils.py
index b7235bc7..7b67e380 100644
--- a/xclim/utils.py
+++ b/xclim/utils.py
@@ -253,6 +253,16 @@ def _check_units(val, dim):
     if dim is None or val is None:
         return
 
+    if str(val).startswith("UNSET "):
+        from warnings import warn
+
+        warnings.warn(
+            "This index calculation will soon require user-specified thresholds.",
+            FutureWarning,
+            stacklevel=4,
+        )
+        val = str(val).replace("UNSET ", "")
+
     # TODO remove backwards compatibility of int/float thresholds after v1.0 release
     if isinstance(val, (int, float)):
         return


Test patch:
diff --git a/tests/test_indices.py b/tests/test_indices.py
index d48b249a..cd5f7604 100644
--- a/tests/test_indices.py
+++ b/tests/test_indices.py
@@ -220,7 +220,27 @@ class TestDailyFreezeThawCycles:
 
         mn = tasmin_series(mn + K2C)
         mx = tasmax_series(mx + K2C)
-        out = xci.daily_freezethaw_cycles(mx, mn, "M")
+        out = xci.daily_freezethaw_cycles(mx, mn, freq="M")
+        np.testing.assert_array_equal(out[:2], [5, 1])
+        np.testing.assert_array_equal(out[2:], 0)
+
+    def test_zeroed_thresholds(self, tasmin_series, tasmax_series):
+        mn = np.zeros(365)
+        mx = np.zeros(365)
+
+        # 5 days in 1st month
+        mn[10:20] -= 1
+        mx[10:15] += 1
+
+        # 1 day in 2nd month
+        mn[40:44] += [1, 1, -1, -1]
+        mx[40:44] += [1, -1, 1, -1]
+
+        mn = tasmin_series(mn + K2C)
+        mx = tasmax_series(mx + K2C)
+        out = xci.daily_freezethaw_cycles(
+            mx, mn, thresh_tasmax="0 degC", thresh_tasmin="0 degC", freq="M"
+        )
         np.testing.assert_array_equal(out[:2], [5, 1])
         np.testing.assert_array_equal(out[2:], 0)
 
diff --git a/tests/test_temperature.py b/tests/test_temperature.py
index 49f2e668..7d64a219 100644
--- a/tests/test_temperature.py
+++ b/tests/test_temperature.py
@@ -706,13 +706,19 @@ class TestDailyFreezeThaw:
         # put a nan somewhere
         tasmin.values[180, 1, 0] = np.nan
 
-        frzthw = atmos.daily_freezethaw_cycles(tasmax, tasmin, freq="YS")
+        with pytest.warns(FutureWarning) as record:
+            frzthw = atmos.daily_freezethaw_cycles(tasmax, tasmin, freq="YS")
 
         min1 = tasmin.values[:, 0, 0]
         max1 = tasmax.values[:, 0, 0]
 
         frzthw1 = ((min1 < K2C) * (max1 > K2C) * 1.0).sum()
 
+        assert (
+            "This index calculation will soon require user-specified thresholds."
+            in [str(q.message) for q in record]
+        )
+
         assert np.allclose(frzthw1, frzthw.values[0, 0, 0])
 
         assert np.isnan(frzthw.values[0, 1, 0])
@@ -729,13 +735,25 @@ class TestDailyFreezeThaw:
         # put a nan somewhere
         tasmin.values[180, 1, 0] = np.nan
 
-        frzthw = atmos.daily_freezethaw_cycles(tasmax, tasmin, freq="YS")
+        with pytest.warns(None) as record:
+            frzthw = atmos.daily_freezethaw_cycles(
+                tasmax,
+                tasmin,
+                thresh_tasmax="0 degC",
+                thresh_tasmin="0 degC",
+                freq="YS",
+            )
 
         min1 = tasmin.values[:, 0, 0]
         max1 = tasmax.values[:, 0, 0]
 
         frzthw1 = ((min1 < 0) * (max1 > 0) * 1.0).sum()
 
+        assert (
+            "This index calculation will soon require user-specified thresholds."
+            not in [str(q.message) for q in record]
+        )
+
         assert np.allclose(frzthw1, frzthw.values[0, 0, 0])
 
         assert np.isnan(frzthw.values[0, 1, 0])