Skip to content

Commit 24e5444

Browse files
committed
Add new savgol bounary method
1 parent b177a08 commit 24e5444

File tree

1 file changed

+33
-15
lines changed

1 file changed

+33
-15
lines changed

_delphi_utils_python/delphi_utils/smooth.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def __init__(
5757
impute=True,
5858
minval=None,
5959
poly_fit_degree=3,
60-
boundary_method="identity",
60+
boundary_method="shortened_window",
6161
savgol_weighted=False,
6262
):
6363
self.method_name = method_name
@@ -135,16 +135,15 @@ def moving_average_smoother(
135135
return signal_smoothed
136136

137137
def left_gauss_linear_smoother(
138-
self, signal: np.ndarray, gaussian_bandwidth=10, impute=False, minval=None,
138+
self, signal: np.ndarray, gaussian_bandwidth=100, impute=False, minval=None,
139139
) -> np.ndarray:
140140
"""
141141
Smooth the y-values using a local linear regression with Gaussian weights.
142142
143143
At each time t, we use the data from times 1, ..., t-dt, weighted
144144
using the Gaussian kernel, to produce the estimate at time t.
145145
146-
For math details, see the smoothing_docs folder. TL;DR: the A matrix in the
147-
code below is the design matrix of the regression.
146+
For math details, see the smoothing_docs folder.
148147
149148
Parameters
150149
----------
@@ -161,7 +160,7 @@ def left_gauss_linear_smoother(
161160
"""
162161
n = len(signal)
163162
signal_smoothed = np.zeros_like(signal)
164-
A = np.vstack([np.ones(n), np.arange(n)]).T
163+
A = np.vstack([np.ones(n), np.arange(n)]).T # the regression design matrix
165164
for idx in range(n):
166165
weights = np.exp(-((np.arange(idx + 1) - idx) ** 2) / gaussian_bandwidth)
167166
AwA = np.dot(A[: (idx + 1), :].T * weights, A[: (idx + 1), :])
@@ -178,7 +177,7 @@ def left_gauss_linear_smoother(
178177
return signal_smoothed
179178

180179
def causal_savgol_coeffs(
181-
self, nl, nr, poly_fit_degree, weighted=False, gaussian_bandwidth=10
180+
self, nl, nr, poly_fit_degree, weighted=False, gaussian_bandwidth=100
182181
):
183182
"""
184183
Solves for the Savitzky-Golay coefficients. The coefficients c_i
@@ -229,7 +228,7 @@ def causal_savgol_coeffs(
229228
coeffs[i] = (mat_inverse @ basis_vector)[0]
230229
return coeffs
231230

232-
def pad_and_convolve(self, signal, coeffs, boundary_method="identity"):
231+
def pad_and_convolve(self, signal, coeffs, boundary_method="shortened_window"):
233232
"""
234233
Returns a specific type of convolution of the 1D signal with the 1D signal
235234
coeffs, respecting boundary effects. That is, the output y is
@@ -258,19 +257,38 @@ def pad_and_convolve(self, signal, coeffs, boundary_method="identity"):
258257

259258
# reverse because np.convolve reverses the second argument
260259
temp_reversed_coeffs = np.array(list(reversed(coeffs)))
260+
261+
signal_padded = np.append(np.nan * np.ones(len(coeffs) - 1), signal)
262+
signal_smoothed = np.convolve(signal_padded, temp_reversed_coeffs, mode="valid")
263+
264+
# this section handles the smoothing behavior at the (left) boundary:
265+
# - identity keeps the original signal (doesn't smooth)
266+
# - shortened_window applies savgol with a smaller window to do the fit
267+
# - nan writes nans
261268
if boundary_method == "identity":
262-
signal_padded = np.append(np.nan * np.ones(len(coeffs) - 1), signal)
263-
signal_smoothed = np.convolve(
264-
signal_padded, temp_reversed_coeffs, mode="valid"
265-
)
266269
for ix in range(len(coeffs)):
267270
signal_smoothed[ix] = signal[ix]
268271
return signal_smoothed
272+
elif boundary_method == "shortened_window":
273+
for ix in range(len(coeffs)):
274+
if ix == 0:
275+
signal_smoothed[ix] = signal[ix]
276+
else:
277+
try:
278+
ix_coeffs = self.causal_savgol_coeffs(
279+
-ix,
280+
0,
281+
self.poly_fit_degree,
282+
weighted=self.savgol_weighted,
283+
gaussian_bandwidth=self.gaussian_bandwidth,
284+
)
285+
signal_smoothed[ix] = ix_coeffs @ signal[: (ix + 1)]
286+
except (
287+
np.linalg.LinAlgError, # for small ix, the design matrix is singular
288+
):
289+
signal_smoothed[ix] = signal[ix]
290+
return signal_smoothed
269291
elif boundary_method == "nan":
270-
signal_padded = np.append(np.nan * np.ones(len(coeffs) - 1), signal)
271-
signal_smoothed = np.convolve(
272-
signal_padded, temp_reversed_coeffs, mode="valid"
273-
)
274292
return signal_smoothed
275293
else:
276294
raise ValueError("Unknown boundary method.")

0 commit comments

Comments
 (0)