"""Class performing zero-offset correction of depth
"""
import logging
import pandas as pd
from skdiveMove.tdrsource import TDRSource
from skdiveMove.core import robjs, pandas2ri, diveMove
from skdiveMove.helpers import _append_xr_attr
logger = logging.getLogger(__name__)
# Add the null handler if importing as library; whatever using this library
# should set up logging.basicConfig() as needed
logger.addHandler(logging.NullHandler())
class ZOC(TDRSource):
"""Perform zero offset correction
Attributes
----------
zoc_params
depth_zoc
zoc_method : str
Name of the ZOC method used.
zoc_filters : pandas.DataFrame
DataFrame with output filters for method="filter"
Notes
-----
See ``help(ZOC)`` for inherited attributes. This class extends
:class:`TDRSource`.
"""
def __init__(self, *args, **kwargs):
"""Initialize ZOC instance
Parameters
----------
*args
Positional arguments passed to :meth:`TDRSource.__init__`
**kwargs
Keyword arguments passed to :meth:`TDRSource.__init__`
"""
TDRSource.__init__(self, *args, **kwargs)
self.zoc_method = None
self._zoc_params = None
self._depth_zoc = None
self.zoc_filters = None
def __str__(self):
base = TDRSource.__str__(self)
meth, params = self.zoc_params
return (base +
("\n{0:<20} {1}\n{2:<20} {3}"
.format("ZOC method:", meth, "ZOC parameters:", params)))
def _offset_depth(self, offset=0):
"""Perform ZOC with "offset" method
Parameters
----------
offset : float, optional
Value to subtract from measured depth.
"""
# Retrieve copy of depth from our own property
depth = self.depth
self.zoc_method = "offset"
self._zoc_params = dict(offset=offset)
depth_zoc = depth - offset
depth_zoc[depth_zoc < 0] = 0
_append_xr_attr(depth_zoc, "history", "ZOC")
self._depth_zoc = depth_zoc
def _filter_depth(self, k, probs, depth_bounds=None, na_rm=True):
"""Perform ZOC with "filter" method
Parameters
----------
k : array_like
probs : array_like
**kwargs
Optional keyword arguments; for this method: ('depth_bounds'
(defaults to range), 'na_rm' (defaults to True)).
"""
self.zoc_method = "filter"
# Retrieve copy of depth from our own property
depth = self.depth
depth_ser = depth.to_series()
self._zoc_params = dict(k=k, probs=probs, depth_bounds=depth_bounds,
na_rm=na_rm)
depthmtx = self._depth_filter_r(depth_ser, **self._zoc_params)
depth_zoc = depthmtx.pop("depth_adj")
depth_zoc[depth_zoc < 0] = 0
depth_zoc = depth_zoc.rename("depth").to_xarray()
depth_zoc.attrs = depth.attrs
_append_xr_attr(depth_zoc, "history", "ZOC")
self._depth_zoc = depth_zoc
self.zoc_filters = depthmtx
def zoc(self, method="filter", **kwargs):
"""Apply zero offset correction to depth measurements
This procedure is required to correct drifts in the pressure
transducer of TDR records and noise in depth measurements. Three
methods are available to perform this correction.
Method `offset` can be used when the offset is known in advance,
and this value is used to correct the entire time series.
Therefore, ``offset=0`` specifies no correction.
Method `filter` implements a smoothing/filtering mechanism where
running quantiles can be applied to depth measurements in a
recursive manner [3]_. The method calculates the first running
quantile defined by the first probability in a given sequence on a
moving window of size specified by the first integer supplied in a
second sequence. The next running quantile, defined by the second
supplied probability and moving window size, is applied to the
smoothed/filtered depth measurements from the previous step, and so
on. The corrected depth measurements (d) are calculated as:
.. math::
d = d_{0} - d_{n}
where :math:`d_{0}` is original depth and :math:`d_{n}` is the last
smoothed/filtered depth. This method is under development, but
reasonable results can be achieved by applying two filters (see
Examples). The default `na_rm=True` works well when there are no
level shifts between non-NA phases in the data, but `na_rm=False`
is better in the presence of such shifts. In other words, there is
no reason to pollute the moving window with null values when
non-null phases can be regarded as a continuum, so splicing
non-null phases makes sense. Conversely, if there are level shifts
between non-null phases, then it is better to retain null phases to
help the algorithm recognize the shifts while sliding the
window(s). The search for the surface can be limited to specified
bounds during smoothing/filtering, so that observations outside
these bounds are interpolated using the bounded smoothed/filtered
series.
Once the entire record has been zero-offset corrected, remaining
depths below zero, are set to zero, as these are assumed to
indicate values at the surface.
Parameters
----------
method : {"filter", "offset"}
Name of method to use for zero offset correction.
**kwargs
Optional keyword arguments passed to the chosen method
(:meth:`_offset_depth`, :meth:`_filter_depth`)
References
----------
.. [3] Luque, S.P. and Fried, R. (2011) Recursive filtering for
zero offset correction of diving depth time series. PLoS ONE
6:e15850.
Examples
--------
ZOC using the "offset" method
>>> from skdiveMove.tests import diveMove2skd
>>> tdrX = diveMove2skd()
>>> tdrX.zoc("offset", offset=3)
Using the "filter" method
>>> # Window lengths and probabilities
>>> DB = [-2, 5]
>>> K = [3, 5760]
>>> P = [0.5, 0.02]
>>> tdrX.zoc(k=K, probs=P, depth_bounds=DB)
Plot the filters that were applied
>>> tdrX.plot_zoc(ylim=[-1, 10]) # doctest: +ELLIPSIS
(<Figure ... with 3 Axes>, array([<Axes: ...>,
<Axes: ...>, <Axes: ...>], dtype=object))
"""
if method == "offset":
offset = kwargs.pop("offset", 0)
self._offset_depth(offset)
elif method == "filter":
k = kwargs.pop("k") # must exist
P = kwargs.pop("probs") # must exist
# Default depth bounds equal measured depth range
DB = kwargs.pop("depth_bounds",
[self.depth.min(),
self.depth.max()])
# default as in `_depth_filter`
na_rm = kwargs.pop("na_rm", True)
self._filter_depth(k=k, probs=P, depth_bounds=DB, na_rm=na_rm)
else:
logger.warning("Method {} is not implemented"
.format(method))
logger.info("Finished ZOC")
def _depth_filter_r(self, depth, k, probs, depth_bounds, na_rm=True):
"""Filter method for zero offset correction via `diveMove`
Parameters
----------
depth : pandas.Series
k : array_like
probs : array_like
depth_bounds : array_like
na_rm : bool, optional
Returns
-------
out : pandas.DataFrame
Time-indexed DataFrame with a column for each filter applied, and a
column `depth_adj` for corrected depth.
"""
with (robjs.default_converter + pandas2ri.converter).context():
depthmtx = diveMove._depthFilter(depth,
pd.Series(k), pd.Series(probs),
pd.Series(depth_bounds),
na_rm)
colnames = ["k{0}_p{1}".format(k, p) for k, p in zip(k, probs)]
colnames.append("depth_adj")
return pd.DataFrame(depthmtx, index=depth.index, columns=colnames)
def _get_depth(self):
return self._depth_zoc
depth_zoc = property(_get_depth)
"""Depth array accessor
Returns
-------
xarray.DataArray
"""
def _get_params(self):
return (self.zoc_method, self._zoc_params)
zoc_params = property(_get_params)
"""Parameters used with method for zero-offset correction
Returns
-------
method : str
Method used for ZOC.
params : dict
Dictionary with parameters and values used for ZOC.
"""