Source code for ibllib.dsp.smooth
import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import interp1d
import ibllib.dsp.fourier as ft
[docs]def lp(ts, fac, pad=0.2):
"""
Smooth the data in frequency domain (assumes a uniform sampling rate), using edge padding
ibllib.dsp.smooth.lp(ts, [.1, .15])
:param ts: input signal to be smoothed
:param fac: 2 element vector of the frequency edges relative to Nyquist: [0.15, 0.2] keeps
everything up to 15% of the full band tapering down to 20%
:param pad: padding on the edges of the time serie, between 0 and 1 (0.2 means 20% of the size)
:return: smoothed time series
"""
# keep at least two periods for the padding
lpad = int(np.ceil(ts.shape[0] * pad))
ts_ = np.pad(ts, lpad, mode='edge')
ts_ = ft.lp(ts_, 1, np.array(fac) / 2)
return ts_[lpad:-lpad]
[docs]def rolling_window(x, window_len=11, window='blackman'):
"""
Smooth the data using a window with requested size.
This method is based on the convolution of a scaled window with the signal.
The signal is prepared by introducing reflected copies of the signal
(with the window size) in both ends so that transient parts are minimized
in the beginning and end part of the output signal.
:param x: The input signal
:type x: list or numpy.array
:param window_len: The dimension of the smoothing window,
should be an **odd** integer, defaults to 11
:type window_len: int, optional
:param window: The type of window from ['flat', 'hanning', 'hamming',
'bartlett', 'blackman']
flat window will produce a moving average smoothing,
defaults to 'blackman'
:type window: str, optional
:raises ValueError: Smooth only accepts 1 dimension arrays.
:raises ValueError: Input vector needs to be bigger than window size.
:raises ValueError: Window is not one of 'flat', 'hanning', 'hamming',
'bartlett', 'blackman'
:return: Smoothed array
:rtype: numpy.array
"""
# **NOTE:** length(output) != length(input), to correct this:
# return y[(window_len/2-1):-(window_len/2)] instead of just y.
if isinstance(x, list):
x = np.array(x)
if x.ndim != 1:
raise ValueError("smooth only accepts 1 dimension arrays.")
if x.size < window_len:
raise ValueError("Input vector needs to be bigger than window size.")
if window_len < 3:
return x
if window not in ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']:
raise ValueError("Window is not one of 'flat', 'hanning', 'hamming',\
'bartlett', 'blackman'")
s = np.r_[x[window_len - 1:0:-1], x, x[-1:-window_len:-1]]
# print(len(s))
if window == 'flat': # moving average
w = np.ones(window_len, 'd')
else:
w = eval('np.' + window + '(window_len)')
y = np.convolve(w / w.sum(), s, mode='valid')
return y[round((window_len / 2 - 1)):round(-(window_len / 2))]
[docs]def non_uniform_savgol(x, y, window, polynom):
"""Applies a Savitzky-Golay filter to y with non-uniform spacing as defined in x.
This is based on
https://dsp.stackexchange.com/questions/1676/savitzky-golay-smoothing-filter-for-not-equally-spaced-data
The borders are interpolated like scipy.signal.savgol_filter would do
https://dsp.stackexchange.com/a/64313
Parameters
----------
x : array_like
List of floats representing the x values of the data
y : array_like
List of floats representing the y values. Must have same length as x
window : int (odd)
Window length of datapoints. Must be odd and smaller than x
polynom : int
The order of polynom used. Must be smaller than the window size
Returns
-------
np.array
The smoothed y values
"""
if len(x) != len(y):
raise ValueError('"x" and "y" must be of the same size')
if len(x) < window:
raise ValueError('The data size must be larger than the window size')
if type(window) is not int:
raise TypeError('"window" must be an integer')
if window % 2 == 0:
raise ValueError('The "window" must be an odd integer')
if type(polynom) is not int:
raise TypeError('"polynom" must be an integer')
if polynom >= window:
raise ValueError('"polynom" must be less than "window"')
half_window = window // 2
polynom += 1
# Initialize variables
A = np.empty((window, polynom)) # Matrix
tA = np.empty((polynom, window)) # Transposed matrix
t = np.empty(window) # Local x variables
y_smoothed = np.full(len(y), np.nan)
# Start smoothing
for i in range(half_window, len(x) - half_window, 1):
# Center a window of x values on x[i]
for j in range(0, window, 1):
t[j] = x[i + j - half_window] - x[i]
# Create the initial matrix A and its transposed form tA
for j in range(0, window, 1):
r = 1.0
for k in range(0, polynom, 1):
A[j, k] = r
tA[k, j] = r
r *= t[j]
# Multiply the two matrices
tAA = np.matmul(tA, A)
# Invert the product of the matrices
tAA = np.linalg.inv(tAA)
# Calculate the pseudoinverse of the design matrix
coeffs = np.matmul(tAA, tA)
# Calculate c0 which is also the y value for y[i]
y_smoothed[i] = 0
for j in range(0, window, 1):
y_smoothed[i] += coeffs[0, j] * y[i + j - half_window]
# If at the end or beginning, store all coefficients for the polynom
if i == half_window:
first_coeffs = np.zeros(polynom)
for j in range(0, window, 1):
for k in range(polynom):
first_coeffs[k] += coeffs[k, j] * y[j]
elif i == len(x) - half_window - 1:
last_coeffs = np.zeros(polynom)
for j in range(0, window, 1):
for k in range(polynom):
last_coeffs[k] += coeffs[k, j] * y[len(y) - window + j]
# Interpolate the result at the left border
for i in range(0, half_window, 1):
y_smoothed[i] = 0
x_i = 1
for j in range(0, polynom, 1):
y_smoothed[i] += first_coeffs[j] * x_i
x_i *= x[i] - x[half_window]
# Interpolate the result at the right border
for i in range(len(x) - half_window, len(x), 1):
y_smoothed[i] = 0
x_i = 1
for j in range(0, polynom, 1):
y_smoothed[i] += last_coeffs[j] * x_i
x_i *= x[i] - x[-half_window - 1]
return y_smoothed
[docs]def smooth_interpolate_savgol(signal, window=31, order=3, interp_kind='cubic'):
"""Run savitzy-golay filter on signal, interpolate through nan points.
Parameters
----------
signal : np.ndarray
original noisy signal of shape (t,), may contain nans
window : int
window of polynomial fit for savitzy-golay filter
order : int
order of polynomial for savitzy-golay filter
interp_kind : str
type of interpolation for nans, e.g. 'linear', 'quadratic', 'cubic'
Returns
-------
np.array
smoothed, interpolated signal for each time point, shape (t,)
"""
signal_noisy_w_nans = np.copy(signal)
timestamps = np.arange(signal_noisy_w_nans.shape[0])
good_idxs = np.where(~np.isnan(signal_noisy_w_nans))[0]
# perform savitzky-golay filtering on non-nan points
signal_smooth_nonans = non_uniform_savgol(
timestamps[good_idxs], signal_noisy_w_nans[good_idxs], window=window, polynom=order)
signal_smooth_w_nans = np.copy(signal_noisy_w_nans)
signal_smooth_w_nans[good_idxs] = signal_smooth_nonans
# interpolate nan points
interpolater = interp1d(
timestamps[good_idxs], signal_smooth_nonans, kind=interp_kind, fill_value='extrapolate')
signal = interpolater(timestamps)
return signal
[docs]def smooth_demo():
t = np.linspace(-4, 4, 100)
x = np.sin(t)
xn = x + np.random.randn(len(t)) * 0.1
ws = 31
plt.subplot(211)
plt.plot(np.ones(ws))
windows = ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']
for w in windows[1:]:
eval('plt.plot(np.' + w + '(ws) )')
plt.axis([0, 30, 0, 1.1])
plt.legend(windows)
plt.title("The smoothing windows")
plt.subplot(212)
plt.plot(x)
plt.plot(xn)
for w in windows:
plt.plot(rolling_window(xn, 10, w))
lst = ['original signal', 'signal with noise']
lst.extend(windows)
plt.legend(lst)
plt.title("Smoothing a noisy signal")
plt.ion()
if __name__ == '__main__':
smooth_demo()