Coverage for install/scipp/scipy/signal/__init__.py: 100%
36 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-04-28 01:28 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-04-28 01:28 +0000
1# SPDX-License-Identifier: BSD-3-Clause
2# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
3# @author Simon Heybrock
4"""Sub-package for signal processing such as smoothing.
6This subpackage provides wrappers for a subset of functions from
7:py:mod:`scipy.signal`.
8"""
10from dataclasses import dataclass
11from typing import Union
13from numpy import ndarray
15from ...compat.wrapping import wrap1d
16from ...core import (
17 CoordError,
18 DataArray,
19 UnitError,
20 Variable,
21 array,
22 identical,
23 islinspace,
24 to_unit,
25)
26from ...units import one
29@dataclass
30class SOS:
31 """
32 Second-order sections representation returned by :py:func:`scipp.signal.butter`.
34 This is used as input to :py:func:`scipp.signal.sosfiltfilt`.
35 """
37 coord: Variable
38 sos: ndarray
40 def filtfilt(
41 self, obj: Union[Variable, DataArray], dim: str, **kwargs
42 ) -> DataArray:
43 """
44 Forwards to :py:func:`scipp.signal.sosfiltfilt` with sos argument set to the SOS
45 instance.
46 """
47 return sosfiltfilt(obj, dim=dim, sos=self, **kwargs)
50def _frequency(coord: Variable) -> Variable:
51 if not islinspace(coord).value:
52 raise CoordError("Data is not regularly sampled, cannot compute frequency.")
53 return (len(coord) - 1) / (coord[-1] - coord[0])
56def butter(coord: Variable, *, N: int, Wn: Variable, **kwargs) -> SOS:
57 """
58 Butterworth digital and analog filter design.
60 Design an Nth-order digital or analog Butterworth filter and return the filter
61 coefficients.
63 This is intended for use with :py:func:`scipp.scipy.signal.sosfiltfilt`. See there
64 for an example.
66 This is a wrapper around :py:func:`scipy.signal.butter`. See there for a
67 complete description of parameters. The differences are:
69 - Instead of a sampling frequency fs, this wrapper takes a variable ``coord`` as
70 input. The sampling frequency is then computed from this coordinate. Only data
71 sampled at regular intervals are supported.
72 - The critical frequency or frequencies must be provided as a variable with correct
73 unit.
74 - Only 'sos' output is supported.
76 :seealso: :py:func:`scipp.scipy.signal.sosfiltfilt`
77 """
78 fs = _frequency(coord).value
79 try:
80 Wn = to_unit(Wn, one / coord.unit)
81 except UnitError:
82 raise UnitError(
83 f"Critical frequency unit '{Wn.unit}' incompatible with sampling unit "
84 f"'{one / coord.unit}'"
85 ) from None
86 import scipy.signal
88 return SOS(
89 coord=coord.copy(),
90 sos=scipy.signal.butter(N=N, Wn=Wn.values, fs=fs, output='sos', **kwargs),
91 )
94@wrap1d(keep_coords=True)
95def _sosfiltfilt(da: DataArray, dim: str, *, sos: SOS, **kwargs) -> DataArray:
96 if not identical(da.coords[dim], sos.coord):
97 raise CoordError(
98 f"Coord\n{da.coords[dim]}\nof filter dimension '{dim}' does "
99 f"not match coord\n{sos.coord}\nused for creating the "
100 "second-order sections representation by "
101 "scipp.scipy.signal.butter."
102 )
103 import scipy.signal
105 data = array(
106 dims=da.dims,
107 unit=da.unit,
108 values=scipy.signal.sosfiltfilt(sos.sos, da.values, **kwargs),
109 )
110 return DataArray(data=data)
113def sosfiltfilt(
114 obj: Union[Variable, DataArray], dim: str, *, sos: SOS, **kwargs
115) -> DataArray:
116 """
117 A forward-backward digital filter using cascaded second-order sections.
119 This is a wrapper around :py:func:`scipy.signal.sosfiltfilt`. See there for a
120 complete description of parameters. The differences are:
122 - Instead of an array ``x`` and an optional axis, the input must be a data array
123 (or variable) and dimension label. If it is a variable, the coord used for setting
124 up the second-order sections is used as output coordinate.
125 - The array of second-order filter coefficients ``sos`` includes the coordinate
126 used for computing the frequency used for creating the coefficients. This is
127 compared to the corresponding coordinate of the input data array to ensure that
128 compatible coefficients are used.
130 Examples:
132 .. plot:: :context: close-figs
134 >>> from scipp.scipy.signal import butter, sosfiltfilt
135 >>> x = sc.linspace(dim='x', start=1.1, stop=4.0, num=1000, unit='m')
136 >>> y = sc.sin(x * sc.scalar(1.0, unit='rad/m'))
137 >>> y += sc.sin(x * sc.scalar(400.0, unit='rad/m'))
138 >>> da = sc.DataArray(data=y, coords={'x': x})
139 >>> sos = butter(da.coords['x'], N=4, Wn=20 / x.unit)
140 >>> out = sosfiltfilt(da, 'x', sos=sos)
141 >>> sc.plot({'input':da, 'sosfiltfilt':out})
143 Instead of calling sosfiltfilt the more convenient filtfilt method of
144 :py:class:`scipp.scipy.signal.SOS` can be used:
146 >>> out = butter(da.coords['x'], N=4, Wn=20 / x.unit).filtfilt(da, 'x')
147 """
148 da = (
149 obj
150 if isinstance(obj, DataArray)
151 else DataArray(data=obj, coords={dim: sos.coord})
152 )
153 return _sosfiltfilt(da, dim=dim, sos=sos, **kwargs)
156__all__ = ['butter', 'sosfiltfilt']