Skip to content

Commit 4cf2728

Browse files
authored
Merge pull request #181 from florisvb/more-multi
multidimensional kerneldiff and butterdiff
2 parents d27d2a3 + 088341a commit 4cf2728

File tree

5 files changed

+58
-30
lines changed

5 files changed

+58
-30
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ For more details, read our [Sphinx documentation](https://pynumdiff.readthedocs.
5252
somethingdiff(x, dt, **kwargs)
5353
```
5454

55-
where `x` is data, `dt` is a step size, and various keyword arguments control the behavior. Some methods support variable step size, in which case the second parameter is renamed `dt_or_t` and can receive either a constant step size or an array of values to denote sample locations.
55+
where `x` is data, `dt` is a step size, and various keyword arguments control the behavior. Some methods support variable step size, in which case the second parameter is renamed `dt_or_t` and can receive either a constant step size or an array of values to denote sample locations. Some methods support multidimensional data, in which case there is an `axis` argument to control the dimension differentiated along.
5656

5757
You can set the hyperparameters:
5858
```python

notebooks/6_multidimensionality_demo.ipynb

Lines changed: 27 additions & 9 deletions
Large diffs are not rendered by default.

pynumdiff/smooth_finite_difference.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import scipy.signal
44

55
# included code
6-
from pynumdiff.finite_difference import second_order as finite_difference
6+
from pynumdiff.finite_difference import finitediff
77
from pynumdiff.polynomial_fit import splinediff as _splinediff # patch through
88
from pynumdiff.utils import utility
99

1010

11-
def kerneldiff(x, dt, kernel='friedrichs', window_size=5, num_iterations=1):
11+
def kerneldiff(x, dt, kernel='friedrichs', window_size=5, num_iterations=1, axis=0):
1212
"""Differentiate by applying a smoothing kernel to the signal, then performing 2nd-order finite difference.
1313
:code:`meandiff`, :code:`mediandiff`, :code:`gaussiandiff`, and :code:`friedrichsdiff` call this function.
1414
@@ -18,23 +18,25 @@ def kerneldiff(x, dt, kernel='friedrichs', window_size=5, num_iterations=1):
1818
:code:`'friedrichs'`}
1919
:param int window_size: filtering kernel size
2020
:param int num_iterations: how many times to apply mean smoothing
21+
:param int axis: data dimension along which differentiation is performed
2122
2223
:return: - **x_hat** (np.array) -- estimated (smoothed) x
2324
- **dxdt_hat** (np.array) -- estimated derivative of x
2425
"""
2526
if kernel in ['mean', 'gaussian', 'friedrichs']:
2627
kernel = getattr(utility, f"{kernel}_kernel")(window_size)
27-
x_hat = utility.convolutional_smoother(x, kernel, num_iterations)
28+
x_hat = utility.convolutional_smoother(x, kernel, num_iterations, axis=axis)
2829
elif kernel == 'median':
2930
if not window_size % 2: window_size += 1 # make sure window_size is odd, else medfilt throws error
31+
s = [1]*x.ndim; s[axis] = window_size
3032

3133
x_hat = x
3234
for _ in range(num_iterations):
33-
x_hat = scipy.signal.medfilt(x_hat, window_size)
35+
x_hat = scipy.signal.medfilt(x_hat, s)
3436
else:
3537
raise ValueError("filter_type must be mean, median, gaussian, or friedrichs")
3638

37-
return finite_difference(x_hat, dt)
39+
return finitediff(x_hat, dt, order=2, axis=axis)
3840

3941

4042
def meandiff(x, dt, params=None, options={}, window_size=5, num_iterations=1):
@@ -140,7 +142,7 @@ def friedrichsdiff(x, dt, params=None, options={}, window_size=5, num_iterations
140142
return kerneldiff(x, dt, kernel='friedrichs', window_size=window_size, num_iterations=num_iterations)
141143

142144

143-
def butterdiff(x, dt, params=None, options={}, filter_order=2, cutoff_freq=0.5, num_iterations=1):
145+
def butterdiff(x, dt, params=None, options={}, filter_order=2, cutoff_freq=0.5, num_iterations=1, axis=0):
144146
"""Perform butterworth smoothing on x with scipy.signal.filtfilt followed by second order finite difference
145147
146148
:param np.array[float] x: data to differentiate
@@ -152,6 +154,7 @@ def butterdiff(x, dt, params=None, options={}, filter_order=2, cutoff_freq=0.5,
152154
:param float cutoff_freq: cutoff frequency :math:`\\in [0, 1]`. For a discrete vector, the
153155
value is normalized to the range 0-1, where 1 is the Nyquist frequency.
154156
:param int num_iterations: how many times to apply smoothing
157+
:param int axis: data dimension along which differentiation is performed
155158
156159
:return: - **x_hat** (np.array) -- estimated (smoothed) x
157160
- **dxdt_hat** (np.array) -- estimated derivative of x
@@ -166,11 +169,11 @@ def butterdiff(x, dt, params=None, options={}, filter_order=2, cutoff_freq=0.5,
166169
b, a = scipy.signal.butter(filter_order, cutoff_freq)
167170

168171
x_hat = x
169-
padlen = len(x)-1 if len(x) < 9 else None
172+
padlen = x.shape[axis]-1 if x.shape[axis] < 9 else None
170173
for _ in range(num_iterations):
171-
x_hat = scipy.signal.filtfilt(b, a, x_hat, method="pad", padlen=padlen) # applies forward and backward pass so zero phase
174+
x_hat = scipy.signal.filtfilt(b, a, x_hat, axis=axis, method="pad", padlen=padlen) # applies forward and backward pass so zero phase
172175

173-
return finite_difference(x_hat, dt)
176+
return finitediff(x_hat, dt, order=2, axis=axis)
174177

175178

176179
def splinediff(*args, **kwargs): # pragma: no cover pylint: disable=missing-function-docstring

pynumdiff/tests/test_diff_methods.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
import numpy as np
33
from pytest import mark
44

5+
from ..smooth_finite_difference import kerneldiff, mediandiff, meandiff, gaussiandiff, friedrichsdiff, butterdiff
56
from ..finite_difference import finitediff, first_order, second_order, fourth_order
6-
from ..linear_model import lineardiff
7-
from ..basis_fit import spectraldiff, rbfdiff
87
from ..polynomial_fit import polydiff, savgoldiff, splinediff
8+
from ..basis_fit import spectraldiff, rbfdiff
99
from ..total_variation_regularization import velocity, acceleration, jerk, iterative_velocity, smooth_acceleration
1010
from ..kalman_smooth import rtsdiff, constant_velocity, constant_acceleration, constant_jerk, robustdiff
11-
from ..smooth_finite_difference import mediandiff, meandiff, gaussiandiff, friedrichsdiff, butterdiff
11+
from ..linear_model import lineardiff
1212
# Function aliases for testing cases where parameters change the behavior in a big way, so error limits can be indexed in dict
1313
def iterated_second_order(*args, **kwargs): return second_order(*args, **kwargs)
1414
def iterated_fourth_order(*args, **kwargs): return fourth_order(*args, **kwargs)
@@ -34,13 +34,13 @@ def spline_irreg_step(*args, **kwargs): return splinediff(*args, **kwargs)
3434

3535
# Call both ways, with kwargs (new) and with params list and optional options dict (legacy), to ensure both work
3636
diff_methods_and_params = [
37-
(first_order, {}), (second_order, {}), (fourth_order, {}), # empty dictionary for the case of no parameters
38-
(iterated_second_order, {'num_iterations':5}), (iterated_fourth_order, {'num_iterations':10}),
3937
(meandiff, {'window_size':3, 'num_iterations':2}), (meandiff, [3, 2], {'iterate':True}),
4038
(mediandiff, {'window_size':3, 'num_iterations':2}), (mediandiff, [3, 2], {'iterate':True}),
4139
(gaussiandiff, {'window_size':5}), (gaussiandiff, [5]),
4240
(friedrichsdiff, {'window_size':5}), (friedrichsdiff, [5]),
4341
(butterdiff, {'filter_order':3, 'cutoff_freq':0.7}), (butterdiff, [3, 0.7]),
42+
(first_order, {}), (second_order, {}), (fourth_order, {}), # empty dictionary for the case of no parameters
43+
(iterated_second_order, {'num_iterations':5}), (iterated_fourth_order, {'num_iterations':10}),
4444
(polydiff, {'degree':2, 'window_size':3}), (polydiff, [2, 3]),
4545
(savgoldiff, {'degree':2, 'window_size':5, 'smoothing_win':5}), (savgoldiff, [2, 5, 5]),
4646
(splinediff, {'degree':5, 's':2}), (splinediff, [5, 2]),
@@ -150,7 +150,7 @@ def spline_irreg_step(*args, **kwargs): return splinediff(*args, **kwargs)
150150
[(0, 0), (1, 1), (0, 0), (1, 1)],
151151
[(1, 0), (2, 2), (1, 0), (2, 2)],
152152
[(1, 0), (3, 3), (1, 0), (3, 3)]],
153-
spectraldiff: [[(-15, -15), (-14, -15), (0, -1), (0, 0)],
153+
spectraldiff: [[(-15, -15), (-14, -14), (0, -1), (0, 0)],
154154
[(0, 0), (1, 1), (0, 0), (1, 1)],
155155
[(1, 1), (1, 1), (1, 1), (1, 1)],
156156
[(0, 0), (1, 1), (0, 0), (1, 1)],
@@ -304,13 +304,19 @@ def test_diff_method(diff_method_and_params, test_func_and_deriv, request): # re
304304

305305
# When one day all or most methods support multidimensionality, and the legacy way of calling methods is
306306
# gone, diff_methods_and_params can be used for the multidimensionality test as well
307-
multidim_methods_and_params = [(finitediff, {})]
307+
multidim_methods_and_params = [
308+
(kerneldiff, {'kernel': 'gaussian', 'window_size': 5}),
309+
(butterdiff, {'filter_order': 3, 'cutoff_freq': 1 - 1e-6}),
310+
(finitediff, {}),
311+
]
308312

309313
# Similar to the error_bounds table, index by method first. But then we test against only one 2D function,
310314
# and only in the absence of noise, since the other test covers that. Instead, because multidimensional
311315
# derivatives can be combined in interesting fashions, we find d^2 / dt_1 dt_2 and the Laplacian,
312316
# d^2/dt_1^2 + d^2/dt_2^2. Tuples are again (L2,Linf) distances.
313317
multidim_error_bounds = {
318+
kerneldiff: [(2, 1), (3, 2)],
319+
butterdiff: [(0, -1), (1, -1)],
314320
finitediff: [(0, -1), (1, -1)]
315321
}
316322

@@ -364,3 +370,4 @@ def test_multidimensionality(multidim_method_and_params, request):
364370
ax2.plot_wireframe(T1, T2, computed_d2)
365371
ax3.plot_wireframe(T1, T2, computed_laplacian, label='computed')
366372
legend = ax3.legend(bbox_to_anchor=(0.7, 0.8)); legend.legend_handles[0].set_facecolor(pyplot.cm.viridis(0.6))
373+
fig.suptitle(f'{diff_method.__name__}', fontsize=16)

pynumdiff/utils/utility.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from scipy.integrate import cumulative_trapezoid
44
from scipy.optimize import minimize
55
from scipy.stats import median_abs_deviation, norm
6+
from scipy.ndimage import convolve1d
67

78

89
def huber(x, M):
@@ -95,21 +96,20 @@ def friedrichs_kernel(window_size):
9596
return ker / np.sum(ker)
9697

9798

98-
def convolutional_smoother(x, kernel, num_iterations=1):
99+
def convolutional_smoother(x, kernel, num_iterations=1, axis=0):
99100
"""Perform smoothing by convolving x with a kernel.
100101
101102
:param np.array[float] x: 1D data
102103
:param np.array[float] kernel: kernel to use in convolution
103104
:param int num_iterations: number of iterations, >=1
105+
:param int axis: data dimension along which convolution is performed
104106
105107
:return: **x_hat** (np.array[float]) -- smoothed x
106108
"""
107-
pad_width = len(kernel)//2
108109
x_hat = x
109110

110111
for i in range(num_iterations):
111-
x_padded = np.pad(x_hat, pad_width, mode='symmetric') # pad with repetition of the edges
112-
x_hat = np.convolve(x_padded, kernel, 'valid')[:len(x)] # 'valid' slices out only full-overlap spots
112+
x_hat = convolve1d(x_hat, kernel, axis=axis, mode='reflect') # 'reflect' pads the signal with repeats
113113

114114
return x_hat
115115

0 commit comments

Comments
 (0)