123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357 |
- import numpy as np
- from scipy.linalg import lstsq
- from scipy._lib._util import float_factorial
- from scipy.ndimage import convolve1d
- from ._arraytools import axis_slice
- def savgol_coeffs(window_length, polyorder, deriv=0, delta=1.0, pos=None,
- use="conv"):
- """Compute the coefficients for a 1-D Savitzky-Golay FIR filter.
- Parameters
- ----------
- window_length : int
- The length of the filter window (i.e., the number of coefficients).
- polyorder : int
- The order of the polynomial used to fit the samples.
- `polyorder` must be less than `window_length`.
- deriv : int, optional
- The order of the derivative to compute. This must be a
- nonnegative integer. The default is 0, which means to filter
- the data without differentiating.
- delta : float, optional
- The spacing of the samples to which the filter will be applied.
- This is only used if deriv > 0.
- pos : int or None, optional
- If pos is not None, it specifies evaluation position within the
- window. The default is the middle of the window.
- use : str, optional
- Either 'conv' or 'dot'. This argument chooses the order of the
- coefficients. The default is 'conv', which means that the
- coefficients are ordered to be used in a convolution. With
- use='dot', the order is reversed, so the filter is applied by
- dotting the coefficients with the data set.
- Returns
- -------
- coeffs : 1-D ndarray
- The filter coefficients.
- See Also
- --------
- savgol_filter
- Notes
- -----
- .. versionadded:: 0.14.0
- References
- ----------
- A. Savitzky, M. J. E. Golay, Smoothing and Differentiation of Data by
- Simplified Least Squares Procedures. Analytical Chemistry, 1964, 36 (8),
- pp 1627-1639.
- Jianwen Luo, Kui Ying, and Jing Bai. 2005. Savitzky-Golay smoothing and
- differentiation filter for even number data. Signal Process.
- 85, 7 (July 2005), 1429-1434.
- Examples
- --------
- >>> import numpy as np
- >>> from scipy.signal import savgol_coeffs
- >>> savgol_coeffs(5, 2)
- array([-0.08571429, 0.34285714, 0.48571429, 0.34285714, -0.08571429])
- >>> savgol_coeffs(5, 2, deriv=1)
- array([ 2.00000000e-01, 1.00000000e-01, 2.07548111e-16, -1.00000000e-01,
- -2.00000000e-01])
- Note that use='dot' simply reverses the coefficients.
- >>> savgol_coeffs(5, 2, pos=3)
- array([ 0.25714286, 0.37142857, 0.34285714, 0.17142857, -0.14285714])
- >>> savgol_coeffs(5, 2, pos=3, use='dot')
- array([-0.14285714, 0.17142857, 0.34285714, 0.37142857, 0.25714286])
- >>> savgol_coeffs(4, 2, pos=3, deriv=1, use='dot')
- array([0.45, -0.85, -0.65, 1.05])
- `x` contains data from the parabola x = t**2, sampled at
- t = -1, 0, 1, 2, 3. `c` holds the coefficients that will compute the
- derivative at the last position. When dotted with `x` the result should
- be 6.
- >>> x = np.array([1, 0, 1, 4, 9])
- >>> c = savgol_coeffs(5, 2, pos=4, deriv=1, use='dot')
- >>> c.dot(x)
- 6.0
- """
- # An alternative method for finding the coefficients when deriv=0 is
- # t = np.arange(window_length)
- # unit = (t == pos).astype(int)
- # coeffs = np.polyval(np.polyfit(t, unit, polyorder), t)
- # The method implemented here is faster.
- # To recreate the table of sample coefficients shown in the chapter on
- # the Savitzy-Golay filter in the Numerical Recipes book, use
- # window_length = nL + nR + 1
- # pos = nL + 1
- # c = savgol_coeffs(window_length, M, pos=pos, use='dot')
- if polyorder >= window_length:
- raise ValueError("polyorder must be less than window_length.")
- halflen, rem = divmod(window_length, 2)
- if pos is None:
- if rem == 0:
- pos = halflen - 0.5
- else:
- pos = halflen
- if not (0 <= pos < window_length):
- raise ValueError("pos must be nonnegative and less than "
- "window_length.")
- if use not in ['conv', 'dot']:
- raise ValueError("`use` must be 'conv' or 'dot'")
- if deriv > polyorder:
- coeffs = np.zeros(window_length)
- return coeffs
- # Form the design matrix A. The columns of A are powers of the integers
- # from -pos to window_length - pos - 1. The powers (i.e., rows) range
- # from 0 to polyorder. (That is, A is a vandermonde matrix, but not
- # necessarily square.)
- x = np.arange(-pos, window_length - pos, dtype=float)
- if use == "conv":
- # Reverse so that result can be used in a convolution.
- x = x[::-1]
- order = np.arange(polyorder + 1).reshape(-1, 1)
- A = x ** order
- # y determines which order derivative is returned.
- y = np.zeros(polyorder + 1)
- # The coefficient assigned to y[deriv] scales the result to take into
- # account the order of the derivative and the sample spacing.
- y[deriv] = float_factorial(deriv) / (delta ** deriv)
- # Find the least-squares solution of A*c = y
- coeffs, _, _, _ = lstsq(A, y)
- return coeffs
- def _polyder(p, m):
- """Differentiate polynomials represented with coefficients.
- p must be a 1-D or 2-D array. In the 2-D case, each column gives
- the coefficients of a polynomial; the first row holds the coefficients
- associated with the highest power. m must be a nonnegative integer.
- (numpy.polyder doesn't handle the 2-D case.)
- """
- if m == 0:
- result = p
- else:
- n = len(p)
- if n <= m:
- result = np.zeros_like(p[:1, ...])
- else:
- dp = p[:-m].copy()
- for k in range(m):
- rng = np.arange(n - k - 1, m - k - 1, -1)
- dp *= rng.reshape((n - m,) + (1,) * (p.ndim - 1))
- result = dp
- return result
- def _fit_edge(x, window_start, window_stop, interp_start, interp_stop,
- axis, polyorder, deriv, delta, y):
- """
- Given an N-d array `x` and the specification of a slice of `x` from
- `window_start` to `window_stop` along `axis`, create an interpolating
- polynomial of each 1-D slice, and evaluate that polynomial in the slice
- from `interp_start` to `interp_stop`. Put the result into the
- corresponding slice of `y`.
- """
- # Get the edge into a (window_length, -1) array.
- x_edge = axis_slice(x, start=window_start, stop=window_stop, axis=axis)
- if axis == 0 or axis == -x.ndim:
- xx_edge = x_edge
- swapped = False
- else:
- xx_edge = x_edge.swapaxes(axis, 0)
- swapped = True
- xx_edge = xx_edge.reshape(xx_edge.shape[0], -1)
- # Fit the edges. poly_coeffs has shape (polyorder + 1, -1),
- # where '-1' is the same as in xx_edge.
- poly_coeffs = np.polyfit(np.arange(0, window_stop - window_start),
- xx_edge, polyorder)
- if deriv > 0:
- poly_coeffs = _polyder(poly_coeffs, deriv)
- # Compute the interpolated values for the edge.
- i = np.arange(interp_start - window_start, interp_stop - window_start)
- values = np.polyval(poly_coeffs, i.reshape(-1, 1)) / (delta ** deriv)
- # Now put the values into the appropriate slice of y.
- # First reshape values to match y.
- shp = list(y.shape)
- shp[0], shp[axis] = shp[axis], shp[0]
- values = values.reshape(interp_stop - interp_start, *shp[1:])
- if swapped:
- values = values.swapaxes(0, axis)
- # Get a view of the data to be replaced by values.
- y_edge = axis_slice(y, start=interp_start, stop=interp_stop, axis=axis)
- y_edge[...] = values
- def _fit_edges_polyfit(x, window_length, polyorder, deriv, delta, axis, y):
- """
- Use polynomial interpolation of x at the low and high ends of the axis
- to fill in the halflen values in y.
- This function just calls _fit_edge twice, once for each end of the axis.
- """
- halflen = window_length // 2
- _fit_edge(x, 0, window_length, 0, halflen, axis,
- polyorder, deriv, delta, y)
- n = x.shape[axis]
- _fit_edge(x, n - window_length, n, n - halflen, n, axis,
- polyorder, deriv, delta, y)
- def savgol_filter(x, window_length, polyorder, deriv=0, delta=1.0,
- axis=-1, mode='interp', cval=0.0):
- """ Apply a Savitzky-Golay filter to an array.
- This is a 1-D filter. If `x` has dimension greater than 1, `axis`
- determines the axis along which the filter is applied.
- Parameters
- ----------
- x : array_like
- The data to be filtered. If `x` is not a single or double precision
- floating point array, it will be converted to type ``numpy.float64``
- before filtering.
- window_length : int
- The length of the filter window (i.e., the number of coefficients).
- If `mode` is 'interp', `window_length` must be less than or equal
- to the size of `x`.
- polyorder : int
- The order of the polynomial used to fit the samples.
- `polyorder` must be less than `window_length`.
- deriv : int, optional
- The order of the derivative to compute. This must be a
- nonnegative integer. The default is 0, which means to filter
- the data without differentiating.
- delta : float, optional
- The spacing of the samples to which the filter will be applied.
- This is only used if deriv > 0. Default is 1.0.
- axis : int, optional
- The axis of the array `x` along which the filter is to be applied.
- Default is -1.
- mode : str, optional
- Must be 'mirror', 'constant', 'nearest', 'wrap' or 'interp'. This
- determines the type of extension to use for the padded signal to
- which the filter is applied. When `mode` is 'constant', the padding
- value is given by `cval`. See the Notes for more details on 'mirror',
- 'constant', 'wrap', and 'nearest'.
- When the 'interp' mode is selected (the default), no extension
- is used. Instead, a degree `polyorder` polynomial is fit to the
- last `window_length` values of the edges, and this polynomial is
- used to evaluate the last `window_length // 2` output values.
- cval : scalar, optional
- Value to fill past the edges of the input if `mode` is 'constant'.
- Default is 0.0.
- Returns
- -------
- y : ndarray, same shape as `x`
- The filtered data.
- See Also
- --------
- savgol_coeffs
- Notes
- -----
- Details on the `mode` options:
- 'mirror':
- Repeats the values at the edges in reverse order. The value
- closest to the edge is not included.
- 'nearest':
- The extension contains the nearest input value.
- 'constant':
- The extension contains the value given by the `cval` argument.
- 'wrap':
- The extension contains the values from the other end of the array.
- For example, if the input is [1, 2, 3, 4, 5, 6, 7, 8], and
- `window_length` is 7, the following shows the extended data for
- the various `mode` options (assuming `cval` is 0)::
- mode | Ext | Input | Ext
- -----------+---------+------------------------+---------
- 'mirror' | 4 3 2 | 1 2 3 4 5 6 7 8 | 7 6 5
- 'nearest' | 1 1 1 | 1 2 3 4 5 6 7 8 | 8 8 8
- 'constant' | 0 0 0 | 1 2 3 4 5 6 7 8 | 0 0 0
- 'wrap' | 6 7 8 | 1 2 3 4 5 6 7 8 | 1 2 3
- .. versionadded:: 0.14.0
- Examples
- --------
- >>> import numpy as np
- >>> from scipy.signal import savgol_filter
- >>> np.set_printoptions(precision=2) # For compact display.
- >>> x = np.array([2, 2, 5, 2, 1, 0, 1, 4, 9])
- Filter with a window length of 5 and a degree 2 polynomial. Use
- the defaults for all other parameters.
- >>> savgol_filter(x, 5, 2)
- array([1.66, 3.17, 3.54, 2.86, 0.66, 0.17, 1. , 4. , 9. ])
- Note that the last five values in x are samples of a parabola, so
- when mode='interp' (the default) is used with polyorder=2, the last
- three values are unchanged. Compare that to, for example,
- `mode='nearest'`:
- >>> savgol_filter(x, 5, 2, mode='nearest')
- array([1.74, 3.03, 3.54, 2.86, 0.66, 0.17, 1. , 4.6 , 7.97])
- """
- if mode not in ["mirror", "constant", "nearest", "interp", "wrap"]:
- raise ValueError("mode must be 'mirror', 'constant', 'nearest' "
- "'wrap' or 'interp'.")
- x = np.asarray(x)
- # Ensure that x is either single or double precision floating point.
- if x.dtype != np.float64 and x.dtype != np.float32:
- x = x.astype(np.float64)
- coeffs = savgol_coeffs(window_length, polyorder, deriv=deriv, delta=delta)
- if mode == "interp":
- if window_length > x.shape[axis]:
- raise ValueError("If mode is 'interp', window_length must be less "
- "than or equal to the size of x.")
- # Do not pad. Instead, for the elements within `window_length // 2`
- # of the ends of the sequence, use the polynomial that is fitted to
- # the last `window_length` elements.
- y = convolve1d(x, coeffs, axis=axis, mode="constant")
- _fit_edges_polyfit(x, window_length, polyorder, deriv, delta, axis, y)
- else:
- # Any mode other than 'interp' is passed on to ndimage.convolve1d.
- y = convolve1d(x, coeffs, axis=axis, mode=mode, cval=cval)
- return y
|