Repository: Ouranosinc/xclim
Pull request #46 (2018-10-01 23:57:16)

Check that array has daily frequency
Using infer_freq from pandas. 
Does not support non-standard calendars. I suggest we add that to the list of xarray improvements to develop. 


Patch:
diff --git a/README.rst b/README.rst
index 4b41f374..fa1aebcf 100644
--- a/README.rst
+++ b/README.rst
@@ -15,10 +15,6 @@ xclim
 .. image:: https://coveralls.io/repos/github/Ouranosinc/xclim/badge.svg
         :target: https://coveralls.io/github/Ouranosinc/xclim
 
-.. image:: https://www.codefactor.io/repository/github/ouranosinc/xclim/badge
-   :target: https://www.codefactor.io/repository/github/ouranosinc/xclim
-   :alt: CodeFactor
-
 .. image:: https://readthedocs.org/projects/xclim/badge/?version=latest
         :target: https://xclim.readthedocs.io/en/latest/?badge=latest
         :alt: Documentation Status
diff --git a/setup.cfg b/setup.cfg
index 0768ba68..6431c18a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -38,4 +38,5 @@ test = pytest
 [tool:pytest]
 collect_ignore = ['setup.py']
 addopts = --verbose
-
+filterwarnings =
+    ignore::UserWarning
diff --git a/xclim/checks.py b/xclim/checks.py
index fe29bbc3..9f79106b 100644
--- a/xclim/checks.py
+++ b/xclim/checks.py
@@ -1,16 +1,25 @@
+import datetime as dt
 from functools import wraps
 from warnings import warn
 
+import numpy as np
+import pandas as pd
+
 """
 Dev notes
 ---------
 
+I suggest we use `check` for weak checking, and `assert` for strong checking.
+Weak checking would log problems in a log, while strong checking would raise an error.
+
 `functools.wraps` is used to copy the docstring and the function's original name from the source
 function to the decorated function. This allows sphinx to correctly find and document
 functions.
 """
 
 
+# TODO: Implement pandas infer_freq in xarray with CFTimeIndex.
+
 def check_valid(var, key, expected):
     """Check that a variable's attribute has the expected value. Warn user otherwise."""
     att = getattr(var, key, None)
@@ -20,9 +29,22 @@ def check_valid(var, key, expected):
         warn('Variable has a non-conforming {}. Got `{}`, expected `{}`'.format(key, att, expected))
 
 
-def check_monotonic(var):
-    """Assert that the series is continuous (no jumps in time index)."""
-    if not var.time.to_pandas().is_monotonic:
+def assert_daily(var):
+    """Assert that the series is daily and monotonic (no jumps in time index).
+
+    A ValueError is raised otherwise."""
+    t0, t1 = var.time[:2]
+
+    # This won't work for non-standard calendars. Needs to be implemented in xarray.
+    if pd.infer_freq(var.time.to_pandas()) != 'D':
+        raise ValueError("time series is not recognized as daily.")
+
+    # Check that the first time step is one day.
+    if np.timedelta64(dt.timedelta(days=1)) != (t1 - t0).data:
+        raise ValueError("time series is not daily.")
+
+    # Check that the series has the same time step throughout
+    if not var.time.to_pandas().is_monotonic_increasing:
         raise ValueError("time index is not monotonically increasing.")
 
 
@@ -30,11 +52,14 @@ def check_valid_temperature(var, units):
     """Check that variable is a temperature."""
     check_valid(var, 'standard_name', 'air_temperature')
     check_valid(var, 'units', units)
+    assert_daily(var)
 
 
 def check_valid_discharge(var):
     """Check that the variable is a discharge."""
-    check_valid(var, 'standard_name', 'discharge')
+    #
+    check_valid(var, 'standard_name', 'water_volume_transport_in_river_channel')
+    check_valid(var, 'units', 'm3 s-1')
 
 
 def valid_daily_min_temperature(comp, units='K'):
diff --git a/xclim/indices.py b/xclim/indices.py
index d1d27e03..393cdbbf 100644
--- a/xclim/indices.py
+++ b/xclim/indices.py
@@ -9,7 +9,7 @@ import xarray as xr
 
 from . import run_length as rl
 from .checks import valid_daily_mean_temperature, valid_daily_max_min_temperature, valid_daily_min_temperature, \
-    valid_daily_max_temperature, valid_daily_mean_discharge
+    valid_daily_max_temperature, valid_daily_mean_discharge  # , assert_daily
 
 # Frequencies : YS: year start, QS-DEC: seasons starting in december, MS: month start
 # See http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
@@ -18,7 +18,8 @@ ftomm = np.nan
 
 
 # TODO: Move utility functions to another file.
-
+# TODO: Should we reference the standard vocabulary we're using ?
+# E.g. http://vocab.nerc.ac.uk/collection/P07/current/BHMHISG2/
 
 def first_paragraph(txt):
     """Return the first paragraph of a text."""
@@ -58,10 +59,20 @@ def with_attrs(**func_attrs):
 # ATTENTION: ASSUME ALL INDICES WRONG UNTIL TESTED ! #
 # -------------------------------------------------- #
 
-
+@with_attrs(standard_name='water_volume_transport_in_river_channel', long_name='discharge', units='m3 s-1')
 @valid_daily_mean_discharge
 def base_flow_index(q, freq='YS'):
-    """Return the base flow index, defined as the minimum 7-day average flow divided by the mean flow."""
+    """Base flow index.
+
+    Return the base flow index, defined as the minimum 7-day average flow divided by the mean flow.
+
+    Parameters
+    ----------
+    q : xarray.DataArray
+      The river flow [m³/s].
+    freq : str, optional
+      Resampling frequency.
+    """
     m7 = q.rolling(time=7, center=True).mean(dim='time').resample(time=freq)
     mq = q.resample(time=freq)
 
@@ -69,6 +80,7 @@ def base_flow_index(q, freq='YS'):
     return m7m / mq.mean(dim='time')
 
 
+@with_attrs(standard_name='cold_spell_duration_index', long_name='', units='days')
 @valid_daily_min_temperature
 def cold_spell_duration_index(tasmin, tn10, freq='YS'):
     """Cold spell duration index.


Test patch:
diff --git a/tests/test_checks.py b/tests/test_checks.py
new file mode 100644
index 00000000..e4d40f2b
--- /dev/null
+++ b/tests/test_checks.py
@@ -0,0 +1,32 @@
+import numpy as np
+import pandas as pd
+import pytest
+import xarray as xr
+from xclim.indices import tg_mean
+
+
+def test_assert_daily():
+    n = 365  # one day short of a full year
+    times = pd.date_range('2000-01-01', freq='1D', periods=n)
+    da = xr.DataArray(np.arange(n), [('time', times)])
+    tg_mean(da)
+
+    # Bad frequency
+    with pytest.raises(ValueError):
+        times = pd.date_range('2000-01-01', freq='12H', periods=n)
+        da = xr.DataArray(np.arange(n), [('time', times)])
+        tg_mean(da)
+
+    # Missing one day between the two years
+    with pytest.raises(ValueError):
+        times = pd.date_range('2000-01-01', freq='1D', periods=n)
+        times = times.append(pd.date_range('2001-01-01', freq='1D', periods=n))
+        da = xr.DataArray(np.arange(2*n), [('time', times)])
+        tg_mean(da)
+
+    # Duplicate dates
+    with pytest.raises(ValueError):
+        times = pd.date_range('2000-01-01', freq='1D', periods=n)
+        times = times.append(pd.date_range('2000-12-29', freq='1D', periods=n))
+        da = xr.DataArray(np.arange(2*n), [('time', times)])
+        tg_mean(da)