Repository: Ouranosinc/xclim

Issue:
Extend support to hourly data
Raven will generate hourly streamflow time series, driven by hourly meteorological data. Ideally we would be able to define indicators operating on hourly data. 

Discussion:

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3b23b630..2b76e53c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -42,7 +42,7 @@ repos:
 #    -   id: autopep8
 #        args: ['--global-config=setup.cfg','--in-place']
 -   repo: https://github.com/timothycrosley/isort
-    rev: 5.4.2
+    rev: 5.5.1
     hooks:
     -   id: isort
         language_version: python3
diff --git a/HISTORY.rst b/HISTORY.rst
index d8f58915..e5c70855 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -11,6 +11,7 @@ Breaking changes
   Install with `pip install xclim[gis]` in order to retain the same functionality.
 * The python library `pandoc` is no longer listed as a docs build requirement. Documentation still requires a current
   version of `pandoc` binaries installed at system-level.
+* ANUCLIM indices have seen their `input_freq` parameter renamed to `src_timestep` for clarity.
 
 New features and enhancements
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -20,6 +21,8 @@ New features and enhancements
 * New `generic.parametric_quantile` function taking parameters estimated by `generic.fit` as an input.
 * Add support for using probability weighted moments method in `generic.fit` function. Requires the
   `lmoments3` package, which is not included in dependencies because it is unmaintained. Install manually if needed.
+* Added support for indicators based on hourly (1H) inputs, and a first hourly indicator called `max_pr_intensity`.
+  returning hourly precipitation intensity.
 * Indicator instances can be retrieved through their class with the `get_instance()` class method.
   This allows the use of `xclim.core.indicator.registry` as an instance registry.
 * Indicators now have a `realm` attribute. It must be given when creating indicators outside xclim.
@@ -28,7 +31,7 @@ New features and enhancements
 
 Bug fixes
 ~~~~~~~~~
-Nothing yet.
+None
 
 Internal changes
 ~~~~~~~~~~~~~~~~
diff --git a/docs/notebooks/units.ipynb b/docs/notebooks/units.ipynb
index fa8707c6..399845cf 100644
--- a/docs/notebooks/units.ipynb
+++ b/docs/notebooks/units.ipynb
@@ -32,7 +32,9 @@
    "source": [
     "A lot of effort has been placed into automatic handling of input data units. `xclim` will automatically detect the input variable(s) units (e.g. °C versus °K or mm/s versus mm/day etc.) and adjust on-the-fly in order to calculate indices in the consistent manner. This comes with the obvious caveat that input data requires metadata attribute for units\n",
     "\n",
-    "Our toy temperature dataset comes in units of Kelvins (`\"degK\"`). "
+    "For precipitation data, `xclim` expects precipitation fluxes. This could be units of `length/time`, such as mm/d, or units of `mass / area / time`, such as kg/m²/s. Units of `length` only, such as mm, are not supported, because the interpretation depends on the frequency of the data, which cannot always be inferred explicitly from the data. For example, if a daily precipitation series records total daily precipitation and has units of `mm`, change the units attribute to `mm/d` before computing indicators. Note that `xclim` will automatically convert between `mass / area / time` and `length/time` using a water density of 1000 kg/m³ when the context is hydrology.\n",
+    "\n",
+    "In the following examples, our toy temperature dataset comes in units of Kelvins (`\"degK\"`)."
    ]
   },
   {
@@ -120,7 +122,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.2"
+   "version": "3.6.7"
   }
  },
  "nbformat": 4,
diff --git a/xclim/core/datachecks.py b/xclim/core/datachecks.py
index 6ff0ab26..a4d8a147 100644
--- a/xclim/core/datachecks.py
+++ b/xclim/core/datachecks.py
@@ -8,18 +8,38 @@ Utilities designed to check the validity of data inputs.
 import xarray as xr
 
 from .options import datacheck
-from .utils import ValidationError
+from .utils import ValidationError, wrapped_partial
+
+
+@datacheck
+def check_freq(var, freq, strict=True):
+    """Raise an error if not series has not the expected temporal frequency or is not monotonically increasing.
+
+    Parameters
+    ----------
+    var : xr.DataArray
+      Input array.
+    freq : str
+      The temporal frequency defined using the Pandas frequency strings, e.g. 'A', 'M', 'D', 'H', 'T',
+      'S'. Note that a 3-hourly time series is declared as '3H'.
+    strict : bool
+      Whether or not multiples of the frequency are considered invalid. With `strict` set to False, a '3H' series
+      will not raise an error if freq is set to 'H'.
+    """
+    v_freq = xr.infer_freq(var.time)
+    if v_freq != freq:
+        if (freq in v_freq) and not strict:
+            return
+        raise ValidationError(
+            "Time series has temporal frequency `{v_freq}`, expected `{freq}`."
+        )
 
 
 @datacheck
 def check_daily(var):
-    """Assert that the series is daily and monotonic (no jumps in time index).
+    """Raise an error if not series has a frequency other that daily, or is not monotonically increasing.
 
-    A ValidationError is raised otherwise.
+    Note that this does not check for gaps in the series.
     """
-    if xr.infer_freq(var.time.to_pandas()) != "D":
+    if xr.infer_freq(var.time) != "D":
         raise ValidationError("time series is not recognized as daily.")
-
-    # Check that the series does not go backward in time
-    if not var.indexes["time"].is_monotonic_increasing:
-        raise ValidationError("time index is not monotonically increasing.")
diff --git a/xclim/core/indicator.py b/xclim/core/indicator.py
index 81a019fd..3b5cff06 100644
--- a/xclim/core/indicator.py
+++ b/xclim/core/indicator.py
@@ -174,6 +174,8 @@ class Indicator(IndicatorRegistrar):
     missing: {any, wmo, pct, at_least_n, skip, from_context}
       The name of the missing value method. See `xclim.core.checks.MissingBase` to create new custom methods. If
       None, this will be determined by the global configuration (see `xclim.set_options`). Defaults to "from_context".
+    freq: {"D", "H", None}
+      The expected frequency of the input data. Use None if irrelevant.
     missing_options : dict, None
       Arguments to pass to the `missing` function. If None, this will be determined by the global configuration.
     context: str
@@ -210,6 +212,7 @@ class Indicator(IndicatorRegistrar):
     missing = "from_context"
     missing_options = None
     context = "none"
+    freq = None
 
     # Variable metadata (_cf_names, those that can be lists or strings)
     # A developper should access those through cf_attrs on instances
@@ -647,7 +650,7 @@ class Indicator(IndicatorRegistrar):
         options = self.missing_options or OPTIONS[MISSING_OPTIONS].get(self.missing, {})
 
         # We flag periods according to the missing method.
-        miss = (self._missing(da, freq, options, indexer) for da in args)
+        miss = (self._missing(da, freq, self.freq, options, indexer) for da in args)
 
         return reduce(np.logical_or, miss)
 
@@ -689,7 +692,9 @@ class Indicator2D(Indicator):
 
 
 class Daily(Indicator):
-    """Indicator at Daily frequency."""
+    """Indicator defined for inputs at daily frequency."""
+
+    freq = "D"
 
     @staticmethod
     def datacheck(**das):  # noqa
@@ -698,6 +703,17 @@ class Daily(Indicator):
 
 
 class Daily2D(Daily):
-    """Indicator using two dimensions at Daily frequency."""
+    """Indicator using two dimensions at daily frequency."""
 
     _nvar = 2
+
+
+class Hourly(Indicator):
+    """Indicator defined for inputs at strict hourly frequency, meaning 3-hourly inputs would raise an error."""
+
+    freq = "H"
+
+    @staticmethod
+    def datacheck(**das):  # noqa
+        for key, da in das.items():
+            datachecks.check_freq(da, "H")
diff --git a/xclim/core/locales.py b/xclim/core/locales.py
index 79dbe144..2cc30286 100644
--- a/xclim/core/locales.py
+++ b/xclim/core/locales.py
@@ -59,7 +59,14 @@ import pkg_resources
 
 from .formatting import AttrFormatter
 
-TRANSLATABLE_ATTRS = ["long_name", "description", "comment", "title", "abstract"]
+TRANSLATABLE_ATTRS = [
+    "long_name",
+    "description",
+    "comment",
+    "title",
+    "abstract",
+    "keywords",
+]
 
 
 def list_locales():
diff --git a/xclim/core/missing.py b/xclim/core/missing.py
index 85251ec8..3fea640b 100644
--- a/xclim/core/missing.py
+++ b/xclim/core/missing.py
@@ -46,6 +46,8 @@ __all__ = [
     "register_missing_method",
 ]
 
+_np_timedelta64 = {"D": "timedelta64[D]", "H": "timedelta64[h]"}
+
 
 class MissingBase:
     """Base class used to determined where Indicator outputs should be masked.
@@ -56,13 +58,15 @@ class MissingBase:
     to the registry before using them in an Indicator.
     """
 
-    def __init__(self, da, freq, **indexer):
-        self.null, self.count = self.prepare(da, freq, **indexer)
+    def __init__(self, da, freq, src_timestep, **indexer):
+        if src_timestep is None:
+            raise ValueError("`src_timestep` must be either 'D' or 'H'.")
+        self.null, self.count = self.prepare(da, freq, src_timestep, **indexer)
 
     @classmethod
-    def execute(cls, da, freq, options, indexer):
+    def execute(cls, da, freq, src_timestep, options, indexer):
         """Create the instance and call it in one operation."""
-        obj = cls(da, freq, **indexer)
+        obj = cls(da, freq, src_timestep, **indexer)
         return obj(**options)
 
     @staticmethod
@@ -88,7 +92,7 @@ class MissingBase:
 
         return null
 
-    def prepare(self, da, freq, **indexer):
+    def prepare(self, da, freq, src_timestep, **indexer):
         """Prepare arrays to be fed to the `is_missing` function.
 
         Parameters
@@ -98,6 +102,8 @@ class MissingBase:
         freq : str
           Resampling frequency defining the periods defined in
           http://pandas.pydata.org/pandas-docs/stable/timeseries.html#resampling.
+        src_timestep : {"D", "H"}
+          Expected input frequency.
         **indexer : {dim: indexer, }, optional
           Time attribute and values over which to subset the array. For example, use season='DJF' to select winter
           values, month=1 to select January, or month=[6,7,8] to select summer months. If not indexer is given,
@@ -138,9 +144,9 @@ class MissingBase:
             t1 = str(end_time[-1].date())
             if isinstance(da.indexes["time"], xr.CFTimeIndex):
                 cal = da.time.encoding.get("calendar")
-                t = xr.cftime_range(t0, t1, freq="D", calendar=cal)
+                t = xr.cftime_range(t0, t1, freq=src_timestep, calendar=cal)
             else:
-                t = pd.date_range(t0, t1, freq="D")
+                t = pd.date_range(t0, t1, freq=src_timestep)
 
             sda = xr.DataArray(data=np.ones(len(t)), coords={"time": t}, dims=("time",))
             st = generic.select_time(sda, **indexer)
@@ -150,7 +156,9 @@ class MissingBase:
                 count = st.notnull().sum(dim="time")
 
         else:
-            n = (end_time - start_time).days
+            delta = end_time - start_time
+            n = delta.astype(_np_timedelta64[src_timestep])
+
             if freq:
                 count = xr.DataArray(n.values, coords={"time": c.time}, dims="time")
             else:
@@ -185,9 +193,11 @@ class MissingAny(MissingBase):
     Parameters
     ----------
     da : DataArray
-      Input array at daily frequency.
+      Input array.
     freq : str
       Resampling frequency.
+    src_timestep : {"D", "H"}
+      Expected input frequency.
     **indexer : {dim: indexer, }, optional
       Time attribute and values over which to subset the array. For example, use season='DJF' to select winter
       values,
@@ -222,14 +232,17 @@ class MissingWMO(MissingAny):
     Parameters
     ----------
     da : DataArray
-      Input array at daily frequency.
+      Input array.
     freq : str
       Resampling frequency.
     nm : int
       Number of missing values per month that should not be exceeded.
     nc : int
       Number of consecutive missing values per month that should not be exceeded.
+    src_timestep : {"D"}
+      Expected input frequency. Only daily values are supported.
     **indexer : {dim: indexer, }, optional
+      Time attribute and values over which to subset the array. For example, use season='DJF' to select winter
       Time attribute and values over which to subset the array. For example, use season='DJF' to select winter
       values,
       month=1 to select January, or month=[6,7,8] to select summer months. If not indexer is given, all values are
@@ -241,11 +254,17 @@ class MissingWMO(MissingAny):
       A boolean array set to True if period has missing values.
     """
 
-    def __init__(self, da, freq, **indexer):
+    def __init__(self, da, freq, src_timestep, **indexer):
         # Force computation on monthly frequency
+        if src_timestep != "D":
+            raise ValueError(
+                "WMO method to estimate missing data is only defined for daily series."
+            )
+
         if not freq.startswith("M"):
             raise ValueError
-        super().__init__(da, freq, **indexer)
+
+        super().__init__(da, freq, src_timestep, **indexer)
 
     def is_missing(self, null, count, nm=11, nc=5):
         from xclim.indices import run_length as rl
@@ -273,11 +292,13 @@ class MissingPct(MissingBase):
     Parameters
     ----------
     da : DataArray
-      Input array at daily frequency.
+      Input array.
     freq : str
       Resampling frequency.
     tolerance : float
       Fraction of missing values that is tolerated.
+    src_timestep : {"D", "H"}
+      Expected input frequency.
     **indexer : {dim: indexer, }, optional
       Time attribute and values over which to subset the array. For example, use season='DJF' to select winter
       values,
@@ -309,11 +330,13 @@ class AtLeastNValid(MissingBase):
     Parameters
     ----------
     da : DataArray
-      Input array at daily frequency.
+      Input array.
     freq : str
       Resampling frequency.
     n : int
       Minimum of valid values required.
+    src_timestep : {"D", "H"}
+      Expected input frequency.
     **indexer : {dim: indexer, }, optional
       Time attribute and values over which to subset the array. For example, use season='DJF' to select winter
       values, month=1 to select January, or month=[6,7,8] to select summer months. If not indexer is given,
@@ -340,7 +363,7 @@ class AtLeastNValid(MissingBase):
 
 @register_missing_method("skip")
 class Skip(MissingBase):
-    def __init__(self, da, freq=None, **indexer):
+    def __init__(self, da, freq=None, src_timestep=None, **indexer):
         pass
 
     def is_missing(self, null, count):
@@ -359,13 +382,13 @@ class FromContext(MissingBase):
     """
 
     @classmethod
-    def execute(cls, da, freq, options, indexer):
+    def execute(cls, da, freq, src_timestep, options, indexer):
 
         name = OPTIONS[CHECK_MISSING]
         kls = MISSING_METHODS[name]
         opts = OPTIONS[MISSING_OPTIONS][name]
 
-        return kls(da, freq, **indexer)(**opts)
+        return kls(da, freq, src_timestep, **indexer)(**opts)
 
 
 # --------------------------
@@ -375,25 +398,30 @@ class FromContext(MissingBase):
 # user-friendly. This can also be useful for testing.
 
 
-def missing_any(da, freq, **indexer):  # noqa: D103
-    return MissingAny(da, freq, **indexer)()
+def missing_any(da, freq, src_timestep=None, **indexer):  # noqa: D103
+    src_timestep = src_timestep or xr.infer_freq(da.time)
+    return MissingAny(da, freq, src_timestep, **indexer)()
 
 
-def missing_wmo(da, freq, nm=11, nc=5, **indexer):  # noqa: D103
-    missing = MissingWMO(da, "M", **indexer)(nm=nm, nc=nc)
+def missing_wmo(da, freq, nm=11, nc=5, src_timestep=None, **indexer):  # noqa: D103
+    src_timestep = src_timestep or xr.infer_freq(da.time)
+    missing = MissingWMO(da, "M", src_timestep, **indexer)(nm=nm, nc=nc)
     return missing.resample(time=freq).any()
 
 
-def missing_pct(da, freq, tolerance, **indexer):  # noqa: D103
-    return MissingPct(da, freq, **indexer)(tolerance=tolerance)
+def missing_pct(da, freq, tolerance, src_timestep=None, **indexer):  # noqa: D103
+    src_timestep = src_timestep or xr.infer_freq(da.time)
+    return MissingPct(da, freq, src_timestep, **indexer)(tolerance=tolerance)
 
 
-def at_least_n_valid(da, freq, n=1, **indexer):  # noqa: D103
-    return AtLeastNValid(da, freq, **indexer)(n=n)
+def at_least_n_valid(da, freq, n=1, src_timestep=None, **indexer):  # noqa: D103
+    src_timestep = src_timestep or xr.infer_freq(da.time)
+    return AtLeastNValid(da, freq, src_timestep, **indexer)(n=n)
 
 
-def missing_from_context(da, freq, **indexer):  # noqa: D103
-    return FromContext.execute(da, freq, options={}, indexer=indexer)
+def missing_from_context(da, freq, src_timestep=None, **indexer):  # noqa: D103
+    src_timestep = src_timestep or xr.infer_freq(da.time)
+    return FromContext.execute(da, freq, src_timestep, options={}, indexer=indexer)
 
 
 missing_any.__doc__ = MissingAny.__doc__
diff --git a/xclim/indicators/atmos/_precip.py b/xclim/indicators/atmos/_precip.py
index f991d2d3..53081763 100644
--- a/xclim/indicators/atmos/_precip.py
+++ b/xclim/indicators/atmos/_precip.py
@@ -3,7 +3,7 @@
 from inspect import _empty
 
 from xclim import indices
-from xclim.core.indicator import Daily, Daily2D
+from xclim.core.indicator import Daily, Daily2D, Hourly
 from xclim.core.utils import wrapped_partial
 
 __all__ = [
@@ -15,6 +15,7 @@ __all__ = [
     "maximum_consecutive_dry_days",
     "maximum_consecutive_wet_days",
     "daily_pr_intensity",
+    "max_pr_intensity",
     "precip_accumulation",
     "liquid_precip_accumulation",
     "solid_precip_accumulation",
@@ -27,6 +28,10 @@ class Pr(Daily):
     context = "hydro"
 
 
+class HrPr(Hourly):
+    context = "hydro"
+
+
 class PrTas(Daily2D):
     _nvar = 2
     context = "hydro"
@@ -124,6 +129,18 @@ daily_pr_intensity = Pr(
     compute=indices.daily_pr_intensity,
 )
 
+max_pr_intensity = HrPr(
+    identifier="max_pr_intensity",
+    units="mm/h",
+    standard_name="precipitation",
+    long_name="Maximum precipitation intensity over {window}h duration",
+    description="{freq} maximum precipitation intensity over rolling {window}h window.",
+    cell_methods="time: max",
+    compute=indices.max_pr_intensity,
+    duration="{window}",
+    keywords="IDF curves",
+)
+
 precip_accumulation = Pr(
     identifier="prcptot",
     units="mm",
diff --git a/xclim/indices/_anuclim.py b/xclim/indices/_anuclim.py
index 5475e489..671ebefa 100644
--- a/xclim/indices/_anuclim.py
+++ b/xclim/indices/_anuclim.py
@@ -195,13 +195,13 @@ def precip_seasonality(
 def tg_mean_warmcold_quarter(
     tas: xarray.DataArray,
     op: str = None,
-    input_freq: str = None,
+    src_timestep: str = None,
     freq: str = "YS",
 ):
     r"""ANUCLIM Mean temperature of warmest/coldest quarter.
 
     The warmest (or coldest) quarter of the year is determined, and the mean temperature of this period is
-    calculated.  If the input data frequency is "daily" or "weekly", quarters are defined as 13 week periods,
+    calculated.  If the input data frequency is daily ("D") or weekly ("W"), quarters are defined as 13 week periods,
     otherwise as 3 months.
 
     Parameters
@@ -210,8 +210,8 @@ def tg_mean_warmcold_quarter(
       Mean temperature [℃] or [K] at daily, weekly, or monthly frequency.
     op : str
       Operation to perform:  'warmest' calculate warmest quarter; 'coldest' calculate coldest quarter.
-    input_freq : str
-      Input data time frequency - One of 'daily', 'weekly' or 'monthly'.
+    src_timestep : {'D', 'W', 'M'}
+      Input data time frequency - One of daily, weekly or monthly.
     freq : str
       Resampling frequency; Defaults to "YS".
 
@@ -227,7 +227,7 @@ def tg_mean_warmcold_quarter(
 
     >>> import xclim.indices as xci
     >>> t = xr.open_dataset(path_to_tas_file)
-    >>> t_warm_qrt = xci.tg_mean_warmcold_quarter(tas=t.tas, op='warmest', input_freq='daily')
+    >>> t_warm_qrt = xci.tg_mean_warmcold_quarter(tas=t.tas, op='warmest', src_timestep='daily')
 
     Notes
     -----
@@ -236,7 +236,7 @@ def tg_mean_warmcold_quarter(
     the result with input data with daily frequency as well. As such weekly or monthly input values, if desired,
     should be calculated prior to calling the function.
     """
-    out = _to_quarter(input_freq, tas=tas)
+    out = _to_quarter(src_timestep, tas=tas)
 
     op = _np_ops[op]
     return select_resample_op(out, op, freq)
@@ -247,13 +247,13 @@ def tg_mean_wetdry_quarter(
     tas: xarray.DataArray,
     pr: xarray.DataArray,
     op: str = None,
-    input_freq: str = None,
+    src_timestep: str = None,
     freq: str = "YS",
 ):
     r"""ANUCLIM Mean temperature of wettest/driest quarter.
 
     The wettest (or driest) quarter of the year is determined, and the mean temperature of this period is calculated.
-    If the input data frequency is "daily" or "weekly" quarters are defined as 13 week periods, otherwise are 3 months.
+    If the input data frequency is daily ("D") or weekly ("W"), quarters are defined as 13 week periods, otherwise are 3 months.
 
     Parameters
     ----------
@@ -263,8 +263,8 @@ def tg_mean_wetdry_quarter(
       Total precipitation rate at daily, weekly, or monthly frequency.
     op : str {'wettest', 'driest'}
       Operation to perform: 'wettest' calculate for the wettest quarter; 'driest' calculate for the driest quarter.
-    input_freq : str
-      Input data time frequency - One of 'daily', 'weekly' or 'monthly'.
+    src_timestep : {'D', 'W', 'M'}
+      Input data time frequency - One of daily, weekly or monthly.
     freq : str
       Resampling frequency; Defaults to "YS".
 
@@ -280,8 +280,8 @@ def tg_mean_wetdry_quarter(
     the result with input data with daily frequency as well. As such weekly or monthly input values, if desired,
     should be calculated prior to calling the function.
     """
-    tas_qrt = _to_quarter(input_freq, tas=tas)
-    pr_qrt = _to_quarter(input_freq, pr=pr)
+    tas_qrt = _to_quarter(src_timestep, tas=tas)
+    pr_qrt = _to_quarter(src_timestep, pr=pr)
 
     xr_op = _xr_argops[op]
     with xarray.set_options(keep_attrs=True):
@@ -292,12 +292,12 @@ def tg_mean_wetdry_quarter(
 
 @declare_units("mm", pr="[precipitation]")
 def prcptot_wetdry_quarter(
-    pr: xarray.DataArray, op: str = None, input_freq: str = None, freq: str = "YS"
+    pr: xarray.DataArray, op: str = None, src_timestep: str = None, freq: str = "YS"
 ):
     r"""ANUCLIM Total precipitation of wettest/driest quarter.
 
     The wettest (or driest) quarter of the year is determined, and the total precipitation of this
-    period is calculated. If the input data frequency is "daily" or "weekly" quarters
+    period is calculated. If the input data frequency is daily ("D") or weekly ("W") quarters
     are defined as 13 week periods, otherwise are 3 months.
 
     Parameters
@@ -306,8 +306,8 @@ def prcptot_wetdry_quarter(
       Total precipitation rate at daily, weekly, or monthly frequency.
     op : str
       Operation to perform :  'wettest' calculate wettest quarter ; 'driest' calculate driest quarter.
-    input_freq : str
-      Input data time frequency - One of 'daily', 'weekly' or 'monthly'.
+    src_timestep : {'D', 'W', 'M'}
+      Input data time frequency - One of daily, weekly or monthly.
     freq : str
       Resampling frequency; Defaults to "YS".
 
@@ -322,7 +322,7 @@ def prcptot_wetdry_quarter(
 
     >>> from xclim.indices import prcptot_wetdry_quarter
     >>> p = xr.open_dataset(path_to_pr_file)
-    >>> pr_warm_qrt = prcptot_wetdry_quarter(pr=p.pr, op='wettest', input_freq='daily')
+    >>> pr_warm_qrt = prcptot_wetdry_quarter(pr=p.pr, op='wettest', src_timestep='D')
 
     Notes
     -----
@@ -331,7 +331,7 @@ def prcptot_wetdry_quarter(
     the result with input data with daily frequency as well. As such weekly or monthly input values, if desired,
     should be calculated prior to calling the function.
     """
-    out = _to_quarter(input_freq, pr=pr)
+    out = _to_quarter(src_timestep, pr=pr)
 
     try:
         op = _np_ops[op]
@@ -348,13 +348,13 @@ def prcptot_warmcold_quarter(
     pr: xarray.DataArray,
     tas: xarray.DataArray,
     op: str = None,
-    input_freq: str = None,
+    src_timestep: str = None,
     freq="YS",
 ):
     r"""ANUCLIM Total precipitation of warmest/coldest quarter.
 
     The warmest (or coldest) quarter of the year is determined, and the total
-    precipitation of this period is calculated.  If the input data frequency is "daily" or "weekly" quarters
+    precipitation of this period is calculated.  If the input data frequency is daily ("D) or weekly ("W"), quarters
     are defined as 13 week periods, otherwise are 3 months.
 
     Parameters
@@ -365,8 +365,8 @@ def prcptot_warmcold_quarter(
       Mean temperature [℃] or [K] at daily, weekly, or monthly frequency.
     op : str
       Operation to perform: 'warmest' calculate for the warmest quarter ; 'coldest' calculate for the coldest quarter.
-    input_freq : str
-      Input data time frequency - One of 'daily', 'weekly' or 'monthly'.
+    src_timestep : {'D', 'W', 'M'}
+      Input data time frequency - One of daily, weekly or monthly.
     freq : str
       Resampling frequency; Defaults to "YS".
 
@@ -383,8 +383,8 @@ def prcptot_warmcold_quarter(
     should be calculated prior to calling the function.
     """
     # determine input data frequency
-    tas_qrt = _to_quarter(input_freq, tas=tas)
-    pr_qrt = _to_quarter(input_freq, pr=pr)
+    tas_qrt = _to_quarter(src_timestep, tas=tas)
+    pr_qrt = _to_quarter(src_timestep, pr=pr)
 
     xr_op = _xr_argops[op]
     with xarray.set_options(keep_attrs=True):
@@ -394,15 +394,15 @@ def prcptot_warmcold_quarter(
 
 
 @declare_units("mm", pr="[precipitation]")
-def prcptot(pr: xarray.DataArray, input_freq: str = None, freq: str = "YS"):
+def prcptot(pr: xarray.DataArray, src_timestep: str = None, freq: str = "YS"):
     r"""ANUCLIM Accumulated total precipitation.
 
     Parameters
     ----------
     pr : xarray.DataArray
       Total precipitation flux [mm d-1], [mm week-1], [mm month-1] or similar.
-    input_freq : str
-      Input data time frequency - One of 'daily', 'weekly' or 'monthly'.
+    src_timestep : {'D', 'W', 'M'}
+      Input data time frequency - One of daily, weekly or monthly.
     freq : str
       Resampling frequency; Defaults to "YS".
 
@@ -417,23 +417,23 @@ def prcptot(pr: xarray.DataArray, input_freq: str = None, freq: str = "YS"):
     values should be at a weekly (or monthly) frequency.  However, the xclim.indices implementation here will calculate
     the result with input data with daily frequency as well.
     """
-    if input_freq == "monthly":
+    if src_timestep == "M":
         pr = pint_multiply(pr, 1 * units.month, "mm")
-    elif input_freq == "weekly":
+    elif src_timestep == "W":
         pr = pint_multiply(pr, 1 * units.week, "mm")
-    elif input_freq == "daily":
+    elif src_timestep == "D":
         pr = pint_multiply(pr, 1 * units.day, "mm")
     else:
         raise NotImplementedError(
-            f'Unknown input time frequency "{input_freq}" : input_freq parameter must be '
-            f'one of "daily", "weekly" or "monthly"'
+            f'Unknown input time frequency "{src_timestep}" : src_timestep parameter must be '
+            f'one of "D", "W" or "M"'
         )
     return pr.resample(time=freq).sum(dim="time", keep_attrs=True)
 
 
 @declare_units("mm", pr="[precipitation]")
 def prcptot_wetdry_period(
-    pr: xarray.DataArray, op: str = None, input_freq: str = None, freq: str = "YS"
+    pr: xarray.DataArray, op: str = None, src_timestep: str = None, freq: str = "YS"
 ):
     r"""ANUCLIM precipitation of the wettest/driest day, week, or month, depending on the time step.
 
@@ -443,8 +443,8 @@ def prcptot_wetdry_period(
       Total precipitation flux [mm d-1], [mm week-1], [mm month-1] or similar.
     op : str
       Operation to perform :  'wettest' calculate wettest period ; 'driest' calculate driest period.
-    input_freq : str
-      Input data time frequency - One of 'daily', 'weekly' or 'monthly'.
+    src_timestep : {'D', 'W', 'M'}
+      Input data time frequency - One of daily, weekly or monthly.
     freq : str
       Resampling frequency; Defaults to "YS".
 
@@ -460,16 +460,16 @@ def prcptot_wetdry_period(
     the result with input data with daily frequency as well. As such weekly or monthly input values, if desired,
     should be calculated prior to calling the function.
     """
-    if input_freq == "monthly":
+    if src_timestep == "M":
         pr = pint_multiply(pr, 1 * units.month, "mm")
-    elif input_freq == "weekly":
+    elif src_timestep == "W":
         pr = pint_multiply(pr, 1 * units.week, "mm")
-    elif input_freq == "daily":
+    elif src_timestep == "D":
         pr = pint_multiply(pr, 1 * units.day, "mm")
     else:
         raise NotImplementedError(
-            f'Unknown input time frequency "{input_freq}" : input_freq parameter must be '
-            f'one of "daily", "weekly" or "monthly"'
+            f'Unknown input time frequency "{src_timestep}" : src_timestep parameter must be '
+            f'one of "D", "W" or "M"'
         )
 
     if op == "wettest":
@@ -540,7 +540,7 @@ def _to_quarter(freq, pr=None, tas=None):
 
     else:
         raise NotImplementedError(
-            f'Unknown input time frequency "{freq}": must be one of "daily", "weekly" or "monthly".'
+            f'Unknown input time frequency "{freq}": must be one of "D", "W" or "M".'
         )
 
     if tas is not None:
diff --git a/xclim/indices/_simple.py b/xclim/indices/_simple.py
index 70db4ba6..2942d68d 100644
--- a/xclim/indices/_simple.py
+++ b/xclim/indices/_simple.py
@@ -28,6 +28,7 @@ __all__ = [
     "ice_days",
     "max_1day_precipitation_amount",
     "max_n_day_precipitation_amount",
+    "max_pr_intensity",
 ]
 
 
@@ -551,3 +552,40 @@ def max_n_day_precipitation_amount(
     out.attrs["units"] = pr.units
     # Adjust values and units to make sure they are daily
     return pint_multiply(out, 1 * units.day, "mm")
+
+
+@declare_units("mm/h", pr="[precipitation]")
+def max_pr_intensity(pr, window: int = 1, freq: str = "YS"):
+    r"""Highest precipitation intensity over a n-hour moving window.
+
+    Calculate the n-hour rolling average of the original hourly total precipitation series
+    and determine the maximum value over each period.
+
+    Parameters
+    ----------
+    pr : xarray.DataArray
+      Hourly precipitation values [Kg m-2 s-1] or [mm]
+    window : int
+      Window size in hours.
+    freq : str
+      Resampling frequency; Defaults to "YS" (yearly).
+
+    Returns
+    -------
+    xarray.DataArray
+      The highest cumulated n-hour precipitation intensity (mm/h) at the given time frequency.
+
+    Examples
+    --------
+    >>> from xclim.indices import max_pr_intensity
+
+    # The following would compute the maximum 6-hour precipitation intensity.
+    # at an annual frequency:
+    # TODO
+    """
+    # Rolling sum of the values
+    arr = pr.rolling(time=window).mean(allow_lazy=True, skipna=False)
+    out = arr.resample(time=freq).max(dim="time", keep_attrs=True)
+
+    out.attrs["units"] = pr.units
+    return out
diff --git a/xclim/locales/fr.json b/xclim/locales/fr.json
index 1c3395eb..c6965d0a 100644
--- a/xclim/locales/fr.json
+++ b/xclim/locales/fr.json
@@ -112,6 +112,14 @@
     "comment": "",
     "abstract": "Maximum de la somme mobile des précipitations quotidiennes pour une période donnée."
   },
+  "MAX_PR_INTENSITY": {
+      "long_name": "Intensité maximale de précipitation sur une durée de {window} h",
+      "description": "Intensité maximale {freq:f} de précipitation sur une fenêtre mobile de {window} h.",
+      "title": "Intensité de précipitation maximale sur une durée et période donnée.",
+      "comment": "",
+      "keywords": "courbes IDF",
+      "abstract": "Maximum de l'intensité de précipitation horaire pour une durée et période donnée."
+  },
   "WETDAYS": {
     "long_name": "Nombre de jours pluvieux (précip >= {thresh})",
     "description": "Nombre {freq:m} de jours où les précipitations sont au-dessus de {thresh}.",


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 4454815e..61615d6a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -92,6 +92,27 @@ def pr_series():
     return _pr_series
 
 
+@pytest.fixture
+def pr_hr_series():
+    """Return hourly time series."""
+
+    def _pr_hr_series(values, start="1/1/2000"):
+        coords = pd.date_range(start, periods=len(values), freq=pd.DateOffset(hours=1))
+        return xr.DataArray(
+            values,
+            coords=[coords],
+            dims="time",
+            name="pr",
+            attrs={
+                "standard_name": "precipitation_flux",
+                "cell_methods": "time: sum over hour",
+                "units": "kg m-2 s-1",
+            },
+        )
+
+    return _pr_hr_series
+
+
 @pytest.fixture
 def pr_ndseries():
     def _pr_series(values, start="1/1/2000"):
diff --git a/tests/test_checks.py b/tests/test_checks.py
index 0879e1ee..c036a992 100644
--- a/tests/test_checks.py
+++ b/tests/test_checks.py
@@ -8,7 +8,7 @@ import pytest
 import xarray as xr
 
 from xclim import set_options
-from xclim.core import cfchecks
+from xclim.core import cfchecks, datachecks
 from xclim.core.utils import ValidationError
 from xclim.indicators.atmos import tg_mean
 
@@ -115,3 +115,23 @@ def test_cf_compliance_options(tas_series, caplog):
     ):
         with set_options(cf_compliance="raise"):
             cfchecks.check_valid_temperature(tas, "degK")
+
+
+class TestDataCheck:
+    def test_check_hourly(self, date_range):
+        tas_attrs = {
+            "units": "K",
+            "standard_name": "air_temperature",
+        }
+
+        n = 100
+        time = date_range("2000-01-01", freq="H", periods=n)
+        da = xr.DataArray(np.random.rand(n), [("time", time)], attrs=tas_attrs)
+        datachecks.check_freq(da, "H")
+
+        time = date_range("2000-01-01", freq="3H", periods=n)
+        da = xr.DataArray(np.random.rand(n), [("time", time)], attrs=tas_attrs)
+        with pytest.raises(ValidationError):
+            datachecks.check_freq(da, "H")
+
+        datachecks.check_freq(da, "H", strict=False)
diff --git a/tests/test_indicators.py b/tests/test_indicators.py
index d45cfc5c..cde600a4 100644
--- a/tests/test_indicators.py
+++ b/tests/test_indicators.py
@@ -18,13 +18,14 @@ from xclim.core.formatting import (
     parse_doc,
     update_history,
 )
-from xclim.core.indicator import Indicator, registry
+
+from xclim.core.indicator import Daily, Indicator, registry
 from xclim.core.units import units
 from xclim.indices import tg_mean
 from xclim.indices.generic import select_time
 
 
-class UniIndTemp(Indicator):
+class UniIndTemp(Daily):
     realm = "atmos"
     identifier = "tmin"
     var_name = "tmin{thresh}"
@@ -41,7 +42,7 @@ class UniIndTemp(Indicator):
         return out.resample(time=freq).mean(keep_attrs=True)
 
 
-class UniIndPr(Indicator):
+class UniIndPr(Daily):
     realm = "atmos"
     identifier = "prmax"
     units = "mm/s"
@@ -53,7 +54,7 @@ class UniIndPr(Indicator):
         return da.resample(time=freq).mean(keep_attrs=True)
 
 
-class UniClim(Indicator):
+class UniClim(Daily):
     realm = "atmos"
     identifier = "clim"
     units = "K"
@@ -64,7 +65,7 @@ class UniClim(Indicator):
         return select.mean(dim="time", keep_attrs=True)
 
 
-class MultiTemp(Indicator):
+class MultiTemp(Daily):
     realm = "atmos"
     identifier = "minmaxtemp"
     var_name = ["tmin", "tmax"]
diff --git a/tests/test_indices.py b/tests/test_indices.py
index e505abd1..ae4e4674 100644
--- a/tests/test_indices.py
+++ b/tests/test_indices.py
@@ -23,6 +23,7 @@ import xarray as xr
 
 from xclim import indices as xci
 from xclim.core.calendar import percentile_doy
+from xclim.core.units import ValidationError
 
 TESTS_HOME = os.path.abspath(os.path.dirname(__file__))
 TESTS_DATA = os.path.join(TESTS_HOME, "testdata")
@@ -248,6 +249,23 @@ class TestDailyPrIntensity:
         np.testing.assert_array_almost_equal(out[0], 2.5)
 
 
+class TestMaxPrIntensity:
+    # Hourly indicator
+    def test_simple(self, pr_hr_series):
+        pr = pr_hr_series(np.zeros(24 * 36))
+        pr[10:22] += np.arange(12)  # kg / m2 / s
+
+        out = xci.max_pr_intensity(pr, window=1, freq="Y")
+        np.testing.assert_array_almost_equal(out[0], 11 * 3600)
+
+        out = xci.max_pr_intensity(pr, window=12, freq="Y")
+        np.testing.assert_array_almost_equal(out[0], 5.5 * 3600)
+
+        pr.attrs["units"] = "mm"
+        with pytest.raises(ValidationError):
+            xci.max_pr_intensity(pr, window=1, freq="Y")
+
+
 class TestLastSpringFrost:
     def test_simple(self, tas_series):
         a = np.zeros(365)
@@ -1014,9 +1032,9 @@ class TestPrecipWettestDriestQuarter:
         a[(a.time.dt.month == 3)] += -1 / 3600 / 24
 
         with pytest.raises(NotImplementedError):
-            xci.prcptot_wetdry_quarter(a, op="wettest", input_freq="toto")
+            xci.prcptot_wetdry_quarter(a, op="wettest", src_timestep="toto")
         with pytest.raises(NotImplementedError):
-            xci.prcptot_wetdry_quarter(a, op="toto", input_freq="daily")
+            xci.prcptot_wetdry_quarter(a, op="toto", src_timestep="D")
 
     def test_simple(self, pr_series):
         a = np.zeros(365 * 2)
@@ -1025,10 +1043,10 @@ class TestPrecipWettestDriestQuarter:
         a[(a.time.dt.month == 9)] += 5 / 3600 / 24
         a[(a.time.dt.month == 3)] += -1 / 3600 / 24
 
-        out = xci.prcptot_wetdry_quarter(a, op="wettest", input_freq="daily")
+        out = xci.prcptot_wetdry_quarter(a, op="wettest", src_timestep="D")
         np.testing.assert_array_almost_equal(out, [241, 241])
 
-        out = xci.prcptot_wetdry_quarter(a, op="driest", input_freq="daily")
+        out = xci.prcptot_wetdry_quarter(a, op="driest", src_timestep="D")
         np.testing.assert_array_almost_equal(out, [60, 60])
 
     def test_weekly_monthly(self, pr_series):
@@ -1040,16 +1058,16 @@ class TestPrecipWettestDriestQuarter:
 
         p_weekly = xci.precip_accumulation(a, freq="7D")
         p_weekly.attrs["units"] = "mm week-1"
-        out = xci.prcptot_wetdry_quarter(p_weekly, op="wettest", input_freq="weekly")
+        out = xci.prcptot_wetdry_quarter(p_weekly, op="wettest", src_timestep="W")
         np.testing.assert_array_almost_equal(out, [241, 241])
-        out = xci.prcptot_wetdry_quarter(p_weekly, op="driest", input_freq="weekly")
+        out = xci.prcptot_wetdry_quarter(p_weekly, op="driest", src_timestep="W")
         np.testing.assert_array_almost_equal(out, [60, 60])
 
         p_month = xci.precip_accumulation(a, freq="MS")
         p_month.attrs["units"] = "mm month-1"
-        out = xci.prcptot_wetdry_quarter(p_month, op="wettest", input_freq="monthly")
+        out = xci.prcptot_wetdry_quarter(p_month, op="wettest", src_timestep="M")
         np.testing.assert_array_almost_equal(out, [242, 242])
-        out = xci.prcptot_wetdry_quarter(p_month, op="driest", input_freq="monthly")
+        out = xci.prcptot_wetdry_quarter(p_month, op="driest", src_timestep="M")
         np.testing.assert_array_almost_equal(out, [58, 59])
 
     def test_convertunits_nondaily(self, pr_series):
@@ -1061,7 +1079,7 @@ class TestPrecipWettestDriestQuarter:
         p_month = xci.precip_accumulation(a, freq="MS")
         p_month_m = p_month / 10
         p_month_m.attrs["units"] = "cm month-1"
-        out = xci.prcptot_wetdry_quarter(p_month_m, op="wettest", input_freq="monthly")
+        out = xci.prcptot_wetdry_quarter(p_month_m, op="wettest", src_timestep="M")
         np.testing.assert_array_almost_equal(out, [242, 242])
 
 
@@ -1088,12 +1106,12 @@ class TestTempWetDryPrecipWarmColdQuarter:
     @pytest.mark.parametrize(
         "freq,units,op,expected",
         [
-            (("D", "daily"), "mm/day", "wettest", [296.22664037, 296.99585849]),
-            (("7D", "weekly"), "mm/week", "wettest", [296.22664037, 296.99585849]),
-            (("MS", "monthly"), "mm/month", "wettest", [296.25598395, 296.98613685]),
-            (("D", "daily"), "mm/day", "driest", [272.161376, 269.31008671]),
-            (("7D", "weekly"), "mm/week", "driest", [272.161376, 269.31008671]),
-            (("MS", "monthly"), "mm/month", "driest", [272.00644843, 269.04077039]),
+            (("D", "D"), "mm/day", "wettest", [296.22664037, 296.99585849]),
+            (("7D", "W"), "mm/week", "wettest", [296.22664037, 296.99585849]),
+            (("MS", "M"), "mm/month", "wettest", [296.25598395, 296.98613685]),
+            (("D", "D"), "mm/day", "driest", [272.161376, 269.31008671]),
+            (("7D", "W"), "mm/week", "driest", [272.161376, 269.31008671]),
+            (("MS", "M"), "mm/month", "driest", [272.00644843, 269.04077039]),
         ],
     )
     @pytest.mark.parametrize("use_dask", [True, False])
@@ -1101,7 +1119,7 @@ class TestTempWetDryPrecipWarmColdQuarter:
         self, tas_series, pr_series, use_dask, freq, units, op, expected
     ):
         tas, pr = self.get_data(tas_series, pr_series)
-        freq, input_freq = freq
+        freq, src_timestep = freq
         pr = xci.precip_accumulation(pr, freq=freq)
         pr.attrs["units"] = units
 
@@ -1114,7 +1132,7 @@ class TestTempWetDryPrecipWarmColdQuarter:
             pr = pr.expand_dims(lat=[0, 1, 2, 3]).chunk({"lat": 1})
 
         out = xci.tg_mean_wetdry_quarter(
-            tas=tas, pr=pr, freq="YS", input_freq=input_freq, op=op
+            tas=tas, pr=pr, freq="YS", src_timestep=src_timestep, op=op
         )
         if use_dask:
             out = out.isel(lat=0)
@@ -1123,24 +1141,24 @@ class TestTempWetDryPrecipWarmColdQuarter:
     @pytest.mark.parametrize(
         "freq,units,op,expected",
         [
-            (("D", "daily"), "mm/day", "warmest", [2021.82232981, 2237.15117103]),
-            (("7D", "weekly"), "mm/week", "warmest", [2021.82232981, 2237.15117103]),
-            (("MS", "monthly"), "mm/month", "warmest", [2038.54763205, 2247.47136629]),
-            (("D", "daily"), "mm/day", "coldest", [311.91895223, 264.50013361]),
-            (("7D", "weekly"), "mm/week", "coldest", [311.91895223, 264.50013361]),
-            (("MS", "monthly"), "mm/month", "coldest", [311.91895223, 259.36682028]),
+            (("D", "D"), "mm/day", "warmest", [2021.82232981, 2237.15117103]),
+            (("7D", "W"), "mm/week", "warmest", [2021.82232981, 2237.15117103]),
+            (("MS", "M"), "mm/month", "warmest", [2038.54763205, 2247.47136629]),
+            (("D", "D"), "mm/day", "coldest", [311.91895223, 264.50013361]),
+            (("7D", "W"), "mm/week", "coldest", [311.91895223, 264.50013361]),
+            (("MS", "M"), "mm/month", "coldest", [311.91895223, 259.36682028]),
         ],
     )
     def test_pr_warmcold(self, tas_series, pr_series, freq, units, op, expected):
         tas, pr = self.get_data(tas_series, pr_series)
-        freq, input_freq = freq
+        freq, src_timestep = freq
         pr = xci.precip_accumulation(pr, freq=freq)
         pr.attrs["units"] = units
 
         tas = xci.tg_mean(tas, freq=freq)
 
         out = xci.prcptot_warmcold_quarter(
-            tas=tas, pr=pr, freq="YS", input_freq=input_freq, op=op
+            tas=tas, pr=pr, freq="YS", src_timestep=src_timestep, op=op
         )
         np.testing.assert_array_almost_equal(out, expected)
 
@@ -1156,23 +1174,23 @@ class TestTempWarmestColdestQuarter:
         a[(a.time.dt.season == "MAM") & (a.time.dt.year == 1972)] += -10
 
         with pytest.raises(NotImplementedError):
-            xci.tg_mean_warmcold_quarter(a, op="warmest", input_freq="toto")
+            xci.tg_mean_warmcold_quarter(a, op="warmest", src_timestep="toto")
 
         with pytest.raises(KeyError):
-            xci.tg_mean_warmcold_quarter(a, op="toto", input_freq="daily")
+            xci.tg_mean_warmcold_quarter(a, op="toto", src_timestep="D")
 
-        out = xci.tg_mean_warmcold_quarter(a, op="warmest", input_freq="daily")
+        out = xci.tg_mean_warmcold_quarter(a, op="warmest", src_timestep="D")
         np.testing.assert_array_almost_equal(out, [294.66648352, 298.15])
 
-        out = xci.tg_mean_warmcold_quarter(a, op="coldest", input_freq="daily")
+        out = xci.tg_mean_warmcold_quarter(a, op="coldest", src_timestep="D")
         np.testing.assert_array_almost_equal(out, [263.42472527, 263.25989011])
 
         t_weekly = xci.tg_mean(a, freq="7D")
-        out = xci.tg_mean_warmcold_quarter(t_weekly, op="coldest", input_freq="weekly")
+        out = xci.tg_mean_warmcold_quarter(t_weekly, op="coldest", src_timestep="W")
         np.testing.assert_array_almost_equal(out, [263.42472527, 263.25989011])
 
         t_month = xci.tg_mean(a, freq="MS")
-        out = xci.tg_mean_warmcold_quarter(t_month, op="coldest", input_freq="monthly")
+        out = xci.tg_mean_warmcold_quarter(t_month, op="coldest", src_timestep="M")
         np.testing.assert_array_almost_equal(out, [263.15, 263.15])
 
     def test_Celsius(self, tas_series):
@@ -1187,10 +1205,10 @@ class TestTempWarmestColdestQuarter:
         ] += -15
         a[(a.time.dt.season == "MAM") & (a.time.dt.year == 1972)] += -10
 
-        out = xci.tg_mean_warmcold_quarter(a, op="warmest", input_freq="daily")
+        out = xci.tg_mean_warmcold_quarter(a, op="warmest", src_timestep="D")
         np.testing.assert_array_almost_equal(out, [21.51648352, 25])
 
-        out = xci.tg_mean_warmcold_quarter(a, op="coldest", input_freq="daily")
+        out = xci.tg_mean_warmcold_quarter(a, op="coldest", src_timestep="D")
         np.testing.assert_array_almost_equal(out, [-14.835165, -9.89011])
 
 
@@ -1206,17 +1224,17 @@ class TestPrcptot:
     @pytest.mark.parametrize(
         "freq,units,expected",
         [
-            (("D", "daily"), "mm/day", [435.0, 442.0]),
-            (("7D", "weekly"), "mm/week", [441.0, 436.0]),
-            (("MS", "monthly"), "mm/month", [435.0, 442.0]),
+            (("D", "D"), "mm/day", [435.0, 442.0]),
+            (("7D", "W"), "mm/week", [441.0, 436.0]),
+            (("MS", "M"), "mm/month", [435.0, 442.0]),
         ],
     )
     def test_simple(self, pr_series, freq, units, expected):
         pr = self.get_data(pr_series)
-        freq, input_freq = freq
+        freq, src_timestep = freq
         pr = xci.precip_accumulation(pr, freq=freq)
         pr.attrs["units"] = units
-        out = xci.prcptot(pr=pr, freq="YS", input_freq=input_freq)
+        out = xci.prcptot(pr=pr, freq="YS", src_timestep=src_timestep)
         np.testing.assert_array_almost_equal(out, expected)
 
 
@@ -1232,20 +1250,22 @@ class TestPrecipWettestDriestPeriod:
     @pytest.mark.parametrize(
         "freq,units,op,expected",
         [
-            (("D", "daily"), "mm/day", "wettest", [11.0, 12.0]),
-            (("D", "daily"), "mm/day", "driest", [1, 1]),
-            (("7D", "weekly"), "mm/week", "wettest", [77, 62]),
-            (("7D", "weekly"), "mm/week", "driest", [7, 7]),
-            (("MS", "monthly"), "mm/month", "wettest", [101, 107]),
-            (("MS", "monthly"), "mm/month", "driest", [28, 29]),
+            (("D", "D"), "mm/day", "wettest", [11.0, 12.0]),
+            (("D", "D"), "mm/day", "driest", [1, 1]),
+            (("7D", "W"), "mm/week", "wettest", [77, 62]),
+            (("7D", "W"), "mm/week", "driest", [7, 7]),
+            (("MS", "M"), "mm/month", "wettest", [101, 107]),
+            (("MS", "M"), "mm/month", "driest", [28, 29]),
         ],
     )
     def test_simple(self, pr_series, freq, units, op, expected):
         pr = self.get_data(pr_series)
-        freq, input_freq = freq
+        freq, src_timestep = freq
         pr = xci.precip_accumulation(pr, freq=freq)
         pr.attrs["units"] = units
-        out = xci.prcptot_wetdry_period(pr=pr, op=op, freq="YS", input_freq=input_freq)
+        out = xci.prcptot_wetdry_period(
+            pr=pr, op=op, freq="YS", src_timestep=src_timestep
+        )
         np.testing.assert_array_almost_equal(out, expected)
 
 
diff --git a/tests/test_missing.py b/tests/test_missing.py
index 836efd6d..bc575fc0 100644
--- a/tests/test_missing.py
+++ b/tests/test_missing.py
@@ -95,14 +95,14 @@ class TestMissingAnyFills:
         t = list(range(31))
         t.pop(5)
         ts2 = ts.isel(time=t)
-        miss = missing.missing_any(ts2, freq=None)
+        miss = missing.missing_any(ts2, freq=None, src_timestep="H")
         np.testing.assert_array_equal(miss, True)
 
         # With indexer
         miss = missing.missing_any(ts, freq=None, month=[7])
         np.testing.assert_array_equal(miss, False)
 
-        miss = missing.missing_any(ts2, freq=None, month=[7])
+        miss = missing.missing_any(ts2, freq=None, month=[7], src_timestep="H")
         np.testing.assert_array_equal(miss, True)
 
     def test_hydro(self):
@@ -115,6 +115,13 @@ class TestMissingAnyFills:
         miss = missing.missing_any(ds.q_sim, freq="YS", season="JJA")
         np.testing.assert_array_equal(miss, False)
 
+    def test_hourly(self, pr_hr_series):
+        a = np.arange(2.0 * 32 * 24)
+        a[5:10] = np.nan
+        pr = pr_hr_series(a)
+        out = missing.missing_any(pr, freq="MS")
+        np.testing.assert_array_equal(out, [True, False, True])
+
 
 class TestMissingWMO:
     def test_missing_days(self, tas_series):
@@ -135,6 +142,12 @@ class TestMissingWMO:
         out = missing.missing_wmo(ts, freq="QS-JAN")
         np.testing.assert_array_equal(out, [True, False, False, True])
 
+    def test_hourly(self, pr_hr_series):
+        pr = pr_hr_series(np.zeros(30))
+
+        with pytest.raises(ValueError):
+            missing.missing_wmo(pr, freq="MS")
+
 
 class TestMissingPct:
     def test_missing_days(self, tas_series):
@@ -146,6 +159,13 @@ class TestMissingPct:
         assert not out[0]
         assert out[1]
 
+    def test_hourly(self, pr_hr_series):
+        a = np.arange(2.0 * 32 * 24)
+        a[5:50] = np.nan
+        pr = pr_hr_series(a)
+        out = missing.missing_pct(pr, freq="MS", tolerance=0.01)
+        np.testing.assert_array_equal(out, [True, False, True])
+
 
 class TestAtLeastNValid:
     def test_at_least_n_valid(self, tas_series):
@@ -155,3 +175,10 @@ class TestAtLeastNValid:
         ts = tas_series(a)
         out = missing.at_least_n_valid(ts, freq="MS", n=20)
         np.testing.assert_array_equal(out[:2], [False, True])
+
+    def test_hourly(self, pr_hr_series):
+        a = np.arange(2.0 * 32 * 24)
+        a[0:240] = np.nan
+        pr = pr_hr_series(a)
+        out = missing.at_least_n_valid(pr, freq="MS", n=25 * 24)
+        np.testing.assert_array_equal(out, [True, False, True])
diff --git a/tests/test_precip.py b/tests/test_precip.py
index 288fbc90..9cbaab3e 100644
--- a/tests/test_precip.py
+++ b/tests/test_precip.py
@@ -193,6 +193,17 @@ class TestDailyIntensity:
         # assert (np.isnan(dis.values[0, -1, -1]))
 
 
+class TestMaxPrIntensity:
+    def test_simple(self, pr_hr_series):
+        pr1 = pr_hr_series(np.zeros(366 * 24))
+        pr1[10:20] += np.arange(10)
+        pr2 = pr_hr_series(np.ones(366 * 24))
+
+        pr = xr.concat([pr1, pr2], dim="site")
+        out = atmos.max_pr_intensity(pr, window=2, freq="Y")
+        np.testing.assert_array_almost_equal(out.isel(time=0), [8.5 * 3600, 3600])
+
+
 class TestMax1Day:
     # testing of wet_day and daily_pr_intensity, both are related
 
diff --git a/tests/test_utils.py b/tests/test_utils.py
index a973fb4f..bf2eb956 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -6,7 +6,7 @@ from inspect import signature
 import numpy as np
 import xarray as xr
 
-from xclim.core.indicator import Indicator
+from xclim.core.indicator import Daily
 from xclim.core.utils import ensure_chunk_size, walk_map, wrapped_partial
 
 
@@ -50,14 +50,15 @@ def test_wrapped_indicator(tas_series):
         out.attrs["units"] = "days"
         return out
 
-    ind1 = Indicator(
+    ind1 = Daily(
         realm="atmos",
         identifier="test_ind1",
         _nvar=1,
         units="days",
         compute=wrapped_partial(indice, tas2=None),
     )
-    ind2 = Indicator(
+
+    ind2 = Daily(
         realm="atmos",
         identifier="test_ind2",
         _nvar=2,