Skip to content

Commit e483a7c

Browse files
authored
Merge pull request #117 from florisvb/jerk-sliding-slide
Jerk sliding slide
2 parents 4cd368f + 62d680b commit e483a7c

6 files changed

Lines changed: 31 additions & 75 deletions

File tree

pynumdiff/linear_model/_linear_model.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def _polydiff(x, dt, polynomial_order, weights=None):
105105
return _polydiff(x, dt, polynomial_order)
106106

107107
kernel = {'gaussian':utility.gaussian_kernel, 'friedrichs':utility.friedrichs_kernel}[kernel](window_size)
108-
return utility.slide_function(_polydiff, x, dt, kernel, polynomial_order, step_size=step_size, pass_weights=True)
108+
return utility.slide_function(_polydiff, x, dt, kernel, polynomial_order, stride=step_size, pass_weights=True)
109109

110110

111111
#############
@@ -312,8 +312,8 @@ def _lineardiff(x, dt, order, gamma, solver=None):
312312

313313
kernel = {'gaussian':utility.gaussian_kernel, 'friedrichs':utility.friedrichs_kernel}[kernel](window_size)
314314

315-
x_hat_forward, _ = utility.slide_function(_lineardiff, x, dt, kernel, order, gamma, step_size=step_size, solver=solver)
316-
x_hat_backward, _ = utility.slide_function(_lineardiff, x[::-1], dt, kernel, order, gamma, step_size=step_size, solver=solver)
315+
x_hat_forward, _ = utility.slide_function(_lineardiff, x, dt, kernel, order, gamma, stride=step_size, solver=solver)
316+
x_hat_backward, _ = utility.slide_function(_lineardiff, x[::-1], dt, kernel, order, gamma, stride=step_size, solver=solver)
317317

318318
# weights
319319
w = np.arange(1, len(x_hat_forward)+1,1)[::-1]

pynumdiff/tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def store_plots(request):
1212
request.config.plots = defaultdict(lambda: pyplot.subplots(6, 2, figsize=(12,7))) # 6 is len(test_funcs_and_derivs)
1313

1414
def pytest_sessionfinish(session, exitstatus):
15+
if not hasattr(session.config, 'plots'): return
1516
for method,(fig,axes) in session.config.plots.items():
1617
axes[-1,-1].legend()
1718
fig.suptitle(method.__name__)

pynumdiff/tests/test_diff_methods.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
from warnings import warn
55

66
from ..linear_model import lineardiff, polydiff, savgoldiff, spectraldiff
7-
from ..total_variation_regularization import velocity, acceleration, jerk, iterative_velocity, smooth_acceleration
7+
from ..total_variation_regularization import velocity, acceleration, jerk, iterative_velocity, smooth_acceleration, jerk_sliding
88
from ..kalman_smooth import constant_velocity, constant_acceleration, constant_jerk
99
from ..smooth_finite_difference import mediandiff, meandiff, gaussiandiff, friedrichsdiff, butterdiff, splinediff
1010
from ..finite_difference import first_order, second_order
1111
# Function aliases for testing cases where parameters change the behavior in a big way
1212
def iterated_first_order(*args, **kwargs): return first_order(*args, **kwargs)
1313

1414
dt = 0.1
15-
t = np.arange(0, 3+dt, dt) # sample locations, including the endpoint
15+
t = np.linspace(0, 3, 31) # sample locations, including the endpoint
1616
tt = np.linspace(0, 3) # full domain, for visualizing denser plots
1717
np.random.seed(7) # for repeatability of the test, so we don't get random failures
1818
noise = 0.05*np.random.randn(*t.shape)
@@ -51,8 +51,8 @@ def iterated_first_order(*args, **kwargs): return first_order(*args, **kwargs)
5151
(acceleration, {'gamma':1}), (acceleration, [1]),
5252
(jerk, {'gamma':10}), (jerk, [10]),
5353
(iterative_velocity, {'num_iterations':5, 'gamma':0.05}), (iterative_velocity, [5, 0.05]),
54-
(smooth_acceleration, {'gamma':2, 'window_size':5}), (smooth_acceleration, [2, 5])
55-
# TODO (jerk_sliding), because with the test cases here (len < 1000) it would just be a duplicate of jerk
54+
(smooth_acceleration, {'gamma':2, 'window_size':5}), (smooth_acceleration, [2, 5]),
55+
(jerk_sliding, {'gamma':1, 'window_size':15}), (jerk_sliding, [1], {'window_size':15})
5656
]
5757

5858
# All the testing methodology follows the exact same pattern; the only thing that changes is the
@@ -184,7 +184,13 @@ def iterated_first_order(*args, **kwargs): return first_order(*args, **kwargs)
184184
[(-2, -2), (-1, -1), (-1, -1), (0, -1)],
185185
[(0, 0), (1, 0), (0, -1), (1, 0)],
186186
[(1, 1), (2, 2), (1, 1), (2, 2)],
187-
[(1, 1), (3, 3), (1, 1), (3, 3)]]
187+
[(1, 1), (3, 3), (1, 1), (3, 3)]],
188+
jerk_sliding: [[(-15, -15), (-16, -17), (0, -1), (1, 0)],
189+
[(-14, -14), (-14, -14), (0, -1), (0, 0)],
190+
[(-14, -14), (-14, -14), (0, -1), (0, 0)],
191+
[(-1, -1), (0, 0), (0, -1), (0, 0)],
192+
[(0, 0), (2, 2), (0, 0), (2, 2)],
193+
[(1, 1), (3, 3), (1, 1), (3, 3)]]
188194
}
189195

190196
# Essentially run the cartesian product of [diff methods] x [test functions] through this one test

pynumdiff/tests/test_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def identity(x, dt): return x, 0 # should come back the same
7373
x = np.arange(100)
7474
kernel = utility.gaussian_kernel(9)
7575

76-
x_hat, dxdt_hat = utility.slide_function(identity, x, 0.1, kernel, step_size=2)
76+
x_hat, dxdt_hat = utility.slide_function(identity, x, 0.1, kernel, stride=2)
7777

7878
assert np.allclose(x, x_hat)
7979

pynumdiff/total_variation_regularization/_total_variation_regularization.py

Lines changed: 11 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def _total_variation_regularized_derivative(x, dt, order, gamma, solver=None):
9797
dxdt_hat = y/dt # y only holds the dx values; to get deriv scale by dt
9898
x_hat = np.cumsum(y) + integration_constants.value[order-1] # smoothed data
9999

100-
dxdt_hat = (dxdt_hat[0:-1] + dxdt_hat[1:])/2 # take first order FD to smooth a touch
100+
dxdt_hat = (dxdt_hat[:-1] + dxdt_hat[1:])/2 # take first order FD to smooth a touch
101101
ddxdt_hat_f = dxdt_hat[-1] - dxdt_hat[-2]
102102
dxdt_hat_f = dxdt_hat[-1] + ddxdt_hat_f # What is this doing? Could we use a 2nd order FD above natively?
103103
dxdt_hat = np.hstack((dxdt_hat, dxdt_hat_f))
@@ -106,7 +106,7 @@ def _total_variation_regularized_derivative(x, dt, order, gamma, solver=None):
106106
d = dxdt_hat[2] - dxdt_hat[1]
107107
dxdt_hat[0] = dxdt_hat[1] - d
108108

109-
return x_hat*std+mean, dxdt_hat*std
109+
return x_hat*std+mean, dxdt_hat*std # derivative is linear, so scale derivative by std
110110

111111

112112
def velocity(x, dt, params=None, options=None, gamma=None, solver=None):
@@ -229,7 +229,7 @@ def smooth_acceleration(x, dt, params=None, options=None, gamma=None, window_siz
229229
return x_hat, dxdt_hat
230230

231231

232-
def jerk_sliding(x, dt, params=None, options=None, gamma=None, solver=None):
232+
def jerk_sliding(x, dt, params=None, options=None, gamma=None, solver=None, window_size=101):
233233
"""Use convex optimization (cvxpy) to solve for the jerk total variation regularized derivative.
234234
235235
:param np.array[float] x: data to differentiate
@@ -239,6 +239,7 @@ def jerk_sliding(x, dt, params=None, options=None, gamma=None, solver=None):
239239
:param float gamma: the regularization parameter
240240
:param str solver: the solver CVXPY should use, 'MOSEK', 'CVXOPT', 'CLARABEL', 'ECOS', etc.
241241
In testing, 'MOSEK' was the most robust. If not given, fall back to CVXPY's default.
242+
:param int window_size: how wide to make the kernel
242243
243244
:return: tuple[np.array, np.array] of\n
244245
- **x_hat** -- estimated (smoothed) x
@@ -250,68 +251,16 @@ def jerk_sliding(x, dt, params=None, options=None, gamma=None, solver=None):
250251
gamma = params[0] if isinstance(params, list) else params
251252
if options != None:
252253
if 'solver' in options: solver = options['solver']
254+
if 'window_size' in options: window_size = options['window_size']
253255
elif gamma == None:
254256
raise ValueError("`gamma` must be given.")
255257

256-
window_size = 1000
257-
stride = 200
258-
259258
if len(x) < window_size:
259+
warn("len(x) <= window_size, calling standard jerk() without sliding")
260260
return _total_variation_regularized_derivative(x, dt, 3, gamma, solver=solver)
261261

262-
# slide the jerk
263-
final_xsmooth = []
264-
final_xdot_hat = []
265-
first_idx = 0
266-
final_idx = first_idx + window_size
267-
last_loop = False
268-
269-
final_weighting = []
270-
271-
try:
272-
while not last_loop:
273-
xsmooth, xdot_hat = _total_variation_regularized_derivative(x[first_idx:final_idx], dt, 3,
274-
gamma, solver=solver)
275-
276-
xsmooth = np.hstack(([0]*first_idx, xsmooth, [0]*(len(x)-final_idx)))
277-
final_xsmooth.append(xsmooth)
278-
279-
xdot_hat = np.hstack(([0]*first_idx, xdot_hat, [0]*(len(x)-final_idx)))
280-
final_xdot_hat.append(xdot_hat)
281-
282-
# blending
283-
w = np.hstack(([0]*first_idx,
284-
np.arange(1, 201)/200,
285-
[1]*(final_idx-first_idx-400),
286-
(np.arange(1, 201)/200)[::-1],
287-
[0]*(len(x)-final_idx)))
288-
final_weighting.append(w)
289-
290-
if final_idx >= len(x):
291-
last_loop = True
292-
else:
293-
first_idx += stride
294-
final_idx += stride
295-
if final_idx > len(x):
296-
final_idx = len(x)
297-
if final_idx - first_idx < 200:
298-
first_idx -= (200 - (final_idx - first_idx))
299-
300-
# normalize columns
301-
weights = np.vstack(final_weighting)
302-
for c in range(weights.shape[1]):
303-
weights[:, c] /= np.sum(weights[:, c])
304-
305-
# weighted sums
306-
xsmooth = np.vstack(final_xsmooth)
307-
xsmooth = np.sum(xsmooth*weights, axis=0)
308-
309-
xdot_hat = np.vstack(final_xdot_hat)
310-
xdot_hat = np.sum(xdot_hat*weights, axis=0)
311-
312-
return xsmooth, xdot_hat
313-
314-
except ValueError:
315-
warn('Solver failed, returning finite difference instead')
316-
from pynumdiff.finite_difference import second_order
317-
return second_order(x, dt)
262+
ramp = window_size//5
263+
kernel = np.hstack((np.arange(1, ramp+1)/ramp, np.ones(window_size - 2*ramp), (np.arange(1, ramp+1)/ramp)[::-1]))
264+
return utility.slide_function(_total_variation_regularized_derivative, x, dt, kernel, 3, gamma, stride=ramp, solver=solver)
265+
266+

pynumdiff/utils/utility.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,15 +176,15 @@ def convolutional_smoother(x, kernel, iterations=1):
176176
return x_hat[len(x):len(x)*2]
177177

178178

179-
def slide_function(func, x, dt, kernel, *args, step_size=1, pass_weights=False, **kwargs):
179+
def slide_function(func, x, dt, kernel, *args, stride=1, pass_weights=False, **kwargs):
180180
"""Slide a smoothing derivative function across a timeseries with specified window size.
181181
182182
:param callable func: name of the function to slide
183183
:param np.array[float] x: data to differentiate
184184
:param float dt: step size
185185
:param np.array[float] kernel: values to weight the sliding window
186186
:param list args: passed to func
187-
:param int step_size: step size for slide (e.g. 1 means slide by 1 step)
187+
:param int stride: step size for slide (e.g. 1 means slide by 1 step)
188188
:param bool pass_weights: whether weights should be passed to func via update to kwargs
189189
:param dict kwargs: passed to func
190190
@@ -195,11 +195,11 @@ def slide_function(func, x, dt, kernel, *args, step_size=1, pass_weights=False,
195195
if len(kernel) % 2 == 0: raise ValueError("Kernel window size should be odd.")
196196
half_window_size = (len(kernel) - 1)//2 # int because len(kernel) is always odd
197197

198-
weights = np.zeros((int(np.ceil(len(x)/step_size)), len(x)))
198+
weights = np.zeros((int(np.ceil(len(x)/stride)), len(x)))
199199
x_hats = np.zeros(weights.shape)
200200
dxdt_hats = np.zeros(weights.shape)
201201

202-
for i,midpoint in enumerate(range(0, len(x), step_size)): # iterate window midpoints
202+
for i,midpoint in enumerate(range(0, len(x), stride)): # iterate window midpoints
203203
# find where to index data and kernel, taking care at edges
204204
window = slice(max(0, midpoint - half_window_size),
205205
min(len(x), midpoint + half_window_size + 1)) # +1 because slicing works [,)

0 commit comments

Comments
 (0)