Skip to content

Commit 7e6519e

Browse files
committed
moved smooth_finite_difference_tests and in the process found a way to enable options dictionaries in my old-style invocations
1 parent 5815158 commit 7e6519e

3 files changed

Lines changed: 86 additions & 115 deletions

File tree

pynumdiff/smooth_finite_difference/_smooth_finite_difference.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,14 @@ def butterdiff(x, dt, params=None, options={}, filter_order=2, cutoff_freq=0.5,
170170
return x_hat, dxdt_hat
171171

172172

173-
def splinediff(x, dt, params=None, options={}, k=3, s=None, num_iterations=1):
173+
def splinediff(x, dt, params=None, options={}, order=3, s=None, num_iterations=1):
174174
"""Perform spline smoothing on x with scipy.interpolate.UnivariateSpline followed by first order finite difference
175175
176176
:param np.array[float] x: array of time series to differentiate
177177
:param float dt: time step size
178178
:param list params: (**deprecated**, prefer :code:`order`, :code:`cutoff_freq`, and :code:`num_iterations`)
179179
:param dict options: (**deprecated**, prefer :code:`num_iterations`) an empty dictionary or {'iterate': (bool)}
180-
:param int k: polynomial order of the spline. A kth order spline can be differentiated k times.
180+
:param int order: polynomial order of the spline. A kth order spline can be differentiated k times.
181181
:param float s: positive smoothing factor used to choose the number of knots. Number of knots will be increased
182182
until the smoothing condition is satisfied: :math:`\\sum_t (x[t] - \\text{spline}[t])^2 \\leq s`
183183
:param int num_iterations: how many times to apply smoothing
@@ -187,17 +187,17 @@ def splinediff(x, dt, params=None, options={}, k=3, s=None, num_iterations=1):
187187
- **dxdt_hat** -- estimated derivative of x
188188
"""
189189
if params != None: # Warning to support old interface for a while. Remove these lines along with params in a future release.
190-
warn("`params` and `options` parameters will be removed in a future version. Use `k`, `s`, and " +
190+
warn("`params` and `options` parameters will be removed in a future version. Use `order`, `s`, and " +
191191
"`num_iterations` instead.", DeprecationWarning)
192-
k, s = params[0:2]
192+
order, s = params[0:2]
193193
if 'iterate' in options and options['iterate']:
194194
num_iterations = params[2]
195195

196196
t = np.arange(0, len(x)*dt, dt)
197197

198198
x_hat = x
199199
for _ in range(num_iterations):
200-
spline = scipy.interpolate.UnivariateSpline(t, x_hat, k=k, s=s)
200+
spline = scipy.interpolate.UnivariateSpline(t, x_hat, k=order, s=s)
201201
x_hat = spline(t)
202202

203203
x_hat, dxdt_hat = finite_difference(x_hat, dt)

pynumdiff/tests/test_diff_methods.py

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
from ..linear_model import lineardiff, polydiff, savgoldiff, spectraldiff
77
from ..total_variation_regularization import velocity, acceleration, jerk, iterative_velocity
88
from ..kalman_smooth import * # constant_velocity, constant_acceleration, constant_jerk, known_dynamics
9-
from ..smooth_finite_difference import * # mediandiff, meandiff, gaussiandiff, friedrichsdiff, butterdiff, splinediff
9+
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
12-
iterated_first_order = lambda *args, **kwargs: first_order(*args, **kwargs)
12+
def iterated_first_order(*args, **kwargs): return first_order(*args, **kwargs)
1313

1414
dt = 0.1
15-
t = np.arange(0, 3, dt) # sample locations
15+
t = np.arange(0, 3+dt, dt) # 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)
@@ -22,70 +22,116 @@
2222
(0, r"$x(t)=1$", lambda t: np.ones(t.shape), lambda t: np.zeros(t.shape)), # constant
2323
(1, r"$x(t)=2t+1$", lambda t: 2*t + 1, lambda t: 2*np.ones(t.shape)), # affine
2424
(2, r"$x(t)=t^2-t+1$", lambda t: t**2 - t + 1, lambda t: 2*t - 1), # quadratic
25-
(3, r"$x(t)=\sin(3t)+1/2$", lambda t: np.sin(3*t) + 1/2, lambda t: 3*np.cos(3*t)), # sinuoidal
25+
(3, r"$x(t)=\sin(3t)+1/2$", lambda t: np.sin(3*t) + 1/2, lambda t: 3*np.cos(3*t)), # sinuoidal
2626
(4, r"$x(t)=e^t\sin(5t)$", lambda t: np.exp(t)*np.sin(5*t), # growing sinusoidal
2727
lambda t: np.exp(t)*(5*np.cos(5*t) + np.sin(5*t))),
2828
(5, r"$x(t)=\frac{\sin(8t)}{(t+0.1)^{3/2}}$", lambda t: np.sin(8*t)/((t + 0.1)**(3/2)), # steep challenger
2929
lambda t: ((0.8 + 8*t)*np.cos(8*t) - 1.5*np.sin(8*t))/(0.1 + t)**(5/2))]
3030

3131
# Call both ways, with kwargs (new) and with params list with default options dict (legacy), to ensure both work
3232
diff_methods_and_params = [
33-
(first_order, None), (iterated_first_order, {'num_iterations':5}),
34-
(second_order, None),
33+
(first_order, {}),
34+
(iterated_first_order, {'num_iterations':5}),
35+
(second_order, {}), # empty dictionary for the case of no parameters
3536
#(lineardiff, {'order':3, 'gamma':5, 'window_size':10, 'solver':'CVXOPT'}),
3637
(polydiff, {'polynomial_order':2, 'window_size':3}), (polydiff, [2, 3]),
3738
(savgoldiff, {'polynomial_order':2, 'window_size':4, 'smoothing_win':4}), (savgoldiff, [2, 4, 4]),
38-
(spectraldiff, {'high_freq_cutoff':0.1}), (spectraldiff, [0.1])
39+
(spectraldiff, {'high_freq_cutoff':0.1}), (spectraldiff, [0.1]),
40+
(mediandiff, {'window_size':3, 'num_iterations':2}), (mediandiff, [3, 2], {'iterate':True}),
41+
(meandiff, {'window_size':3, 'num_iterations':2}), (meandiff, [3, 2], {'iterate':True}),
42+
(gaussiandiff, {'window_size':5}), (gaussiandiff, [5]),
43+
(friedrichsdiff, {'window_size':5}), (friedrichsdiff, [5]),
44+
(butterdiff, {'filter_order':3, 'cutoff_freq':0.074}), (butterdiff, [3, 0.074]),
45+
(splinediff, {'order':5, 's':2}), (splinediff, [5, 2])
3946
]
4047

4148
# All the testing methodology follows the exact same pattern; the only thing that changes is the
4249
# closeness to the right answer various methods achieve with the given parameterizations. So index a
4350
# big ol' table by the method, then the test function, then the pair of quantities we're comparing.
4451
error_bounds = {
4552
first_order: [[(-25, -25), (-25, -25), (0, 0), (1, 1)],
46-
[(-25, -25), (-14, -14), (0, 0), (1, 1)],
53+
[(-25, -25), (-13, -14), (0, 0), (1, 1)],
4754
[(-25, -25), (0, 0), (0, 0), (1, 0)],
4855
[(-25, -25), (0, 0), (0, 0), (1, 1)],
4956
[(-25, -25), (2, 2), (0, 0), (2, 2)],
5057
[(-25, -25), (3, 3), (0, 0), (3, 3)]],
51-
iterated_first_order: [[(-7, -7), (-9, -10), (0, -1), (0, 0)],
52-
[(-5, -5), (-5, -6), (0, -1), (0, 0)],
58+
iterated_first_order: [[(-8, -9), (-25, -25), (0, -1), (0, 0)],
59+
[(-6, -6), (-6, -7), (0, -1), (0, 0)],
5360
[(-1, -1), (0, 0), (0, -1), (0, 0)],
54-
[(0, 0), (1, 1), (0, 0), (1, 1)],
61+
[(0, 0), (1, 0), (0, 0), (1, 0)],
5562
[(1, 1), (2, 2), (1, 1), (2, 2)],
5663
[(1, 1), (3, 3), (1, 1), (3, 3)]],
5764
second_order: [[(-25, -25), (-25, -25), (0, 0), (1, 1)],
58-
[(-25, -25), (-14, -14), (0, 0), (1, 1)],
59-
[(-25, -25), (-13, -14), (0, 0), (1, 1)],
65+
[(-25, -25), (-13, -13), (0, 0), (1, 1)],
66+
[(-25, -25), (-13, -13), (0, 0), (1, 1)],
6067
[(-25, -25), (0, -1), (0, 0), (1, 1)],
6168
[(-25, -25), (1, 1), (0, 0), (1, 1)],
6269
[(-25, -25), (3, 3), (0, 0), (3, 3)]],
6370
#lineardiff: [TBD when #91 is solved],
64-
polydiff: [[(-14, -15), (-14, -14), (0, -1), (1, 1)],
71+
polydiff: [[(-15, -15), (-14, -14), (0, -1), (1, 1)],
72+
[(-14, -14), (-13, -13), (0, -1), (1, 1)],
6573
[(-14, -14), (-13, -13), (0, -1), (1, 1)],
66-
[(-14, -15), (-13, -14), (0, -1), (1, 1)],
6774
[(-2, -2), (0, 0), (0, -1), (1, 1)],
68-
[(0, 0), (2, 1), (0, 0), (2, 1)],
75+
[(0, 0), (1, 1), (0, -1), (1, 1)],
6976
[(0, 0), (3, 3), (0, 0), (3, 3)]],
70-
savgoldiff: [[(-7, -8), (-13, -14), (0, -1), (0, 0)],
71-
[(-7, -8), (-13, -13), (0, -1), (0, 0)],
77+
savgoldiff: [[(-9, -10), (-13, -14), (0, -1), (0, 0)],
78+
[(-9, -10), (-13, -13), (0, -1), (0, 0)],
7279
[(-1, -1), (0, -1), (0, -1), (0, 0)],
7380
[(0, -1), (0, 0), (0, -1), (1, 0)],
7481
[(1, 1), (2, 2), (1, 1), (2, 2)],
7582
[(1, 1), (3, 3), (1, 1), (3, 3)]],
76-
spectraldiff: [[(-7, -8), (-14, -15), (-1, -1), (0, 0)],
83+
spectraldiff: [[(-9, -10), (-14, -15), (-1, -1), (0, 0)],
84+
[(0, 0), (1, 1), (0, 0), (1, 1)],
85+
[(1, 0), (1, 1), (1, 1), (1, 1)],
7786
[(0, 0), (1, 1), (0, 0), (1, 1)],
78-
[(1, 0), (1, 1), (1, 0), (1, 1)],
87+
[(1, 1), (2, 2), (1, 1), (2, 2)],
88+
[(1, 1), (3, 3), (1, 1), (3, 3)]],
89+
mediandiff: [[(-25, -25), (-25, -25), (-1, -1), (0, 0)],
90+
[(0, 0), (1, 1), (0, 0), (1, 1)],
91+
[(0, 0), (1, 1), (0, 0), (1, 1)],
92+
[(-1, -1), (0, 0), (0, 0), (1, 1)],
93+
[(0, 0), (2, 2), (0, 0), (2, 2)],
94+
[(1, 1), (3, 3), (1, 1), (3, 3)]],
95+
meandiff: [[(-25, -25), (-25, -25), (0, -1), (0, 0)],
96+
[(0, 0), (1, 0), (0, 0), (1, 1)],
97+
[(0, 0), (1, 1), (0, 0), (1, 1)],
98+
[(0, 0), (1, 1), (0, 0), (1, 1)],
99+
[(1, 1), (2, 2), (1, 1), (2, 2)],
100+
[(1, 1), (3, 3), (1, 1), (3, 3)]],
101+
gaussiandiff: [[(-14, -15), (-14, -14), (0, -1), (1, 0)],
102+
[(-1, -1), (0, 0), (0, 0), (1, 0)],
79103
[(0, 0), (1, 1), (0, 0), (1, 1)],
104+
[(0, -1), (1, 0), (0, 0), (1, 1)],
80105
[(1, 1), (2, 2), (1, 1), (2, 2)],
81-
[(1, 1), (3, 3), (1, 1), (3, 3)]]
106+
[(1, 1), (3, 3), (1, 1), (3, 3)]],
107+
friedrichsdiff: [[(-25, -25), (-25, -25), (0, -1), (0, 0)],
108+
[(-1, -1), (0, 0), (0, 0), (1, 0)],
109+
[(0, 0), (1, 1), (0, 0), (1, 1)],
110+
[(0, -1), (1, 1), (0, 0), (1, 1)],
111+
[(1, 1), (2, 2), (1, 1), (2, 2)],
112+
[(1, 1), (3, 3), (1, 1), (3, 3)]],
113+
butterdiff: [[(-13, -14), (-13, -13), (0, -1), (0, 0)],
114+
[(0, -1), (0, 0), (0, -1), (0, 0)],
115+
[(0, 0), (1, 1), (0, 0), (1, 1)],
116+
[(1, 0), (1, 1), (1, 0), (1, 1)],
117+
[(2, 2), (3, 2), (2, 2), (3, 2)],
118+
[(2, 1), (3, 3), (2, 1), (3, 3)]],
119+
splinediff: [[(-14, -15), (-14, -14), (-1, -1), (0, 0)],
120+
[(-14, -14), (-13, -14), (-1, -1), (0, 0)],
121+
[(-14, -14), (0, 0), (-1, -1), (0, 0)],
122+
[(0, 0), (1, 1), (0, 0), (1, 1)],
123+
[(1, 0), (2, 2), (1, 0), (2, 2)],
124+
[(1, 0), (3, 3), (1, 0), (3, 3)]]
82125
}
83126

127+
# Essentially run the cartesian product of [diff methods] x [test functions] through this one test
84128
@mark.filterwarnings("ignore::DeprecationWarning") # I want to test the old and new functionality intentionally
85129
@mark.parametrize("diff_method_and_params", diff_methods_and_params)
86130
@mark.parametrize("test_func_and_deriv", test_funcs_and_derivs)
87131
def test_diff_method(diff_method_and_params, test_func_and_deriv, request): # request gives access to context
88-
diff_method, params = diff_method_and_params # unpack
132+
# unpack
133+
diff_method, params = diff_method_and_params[:2]
134+
if len(diff_method_and_params) == 3: options = diff_method_and_params[2] # optionally pass old-style `options` dict
89135
i, latex_name, f, df = test_func_and_deriv
90136

91137
# some methods rely on cvxpy, and we'd like to allow use of pynumdiff without convex optimization
@@ -97,27 +143,30 @@ def test_diff_method(diff_method_and_params, test_func_and_deriv, request): # re
97143
x_noisy = x + noise # add a little noise
98144
dxdt = df(t) # true values of the derivative at samples
99145

100-
# differentiate without and with noise
101-
x_hat, dxdt_hat = diff_method(x, dt, **params) if isinstance(params, dict) else diff_method(x, dt, params) \
102-
if isinstance(params, list) else diff_method(x, dt)
146+
# differentiate without and with noise, accounting for new and old styles of calling functions
147+
x_hat, dxdt_hat = diff_method(x, dt, **params) if isinstance(params, dict) \
148+
else diff_method(x, dt, params) if (isinstance(params, list) and len(diff_method_and_params) < 3) \
149+
else diff_method(x, dt, params, options)
103150
x_hat_noisy, dxdt_hat_noisy = diff_method(x_noisy, dt, **params) if isinstance(params, dict) \
104-
else diff_method(x_noisy, dt, params) if isinstance(params, list) else diff_method(x_noisy, dt)
151+
else diff_method(x_noisy, dt, params) if (isinstance(params, list) and len(diff_method_and_params) < 3) \
152+
else diff_method(x_noisy, dt, params, options)
105153

106154
# check x_hat and x_hat_noisy are close to x and that dxdt_hat and dxdt_hat_noisy are close to dxdt
107155
#print("]\n[", end="")
108156
for j,(a,b) in enumerate([(x,x_hat), (dxdt,dxdt_hat), (x,x_hat_noisy), (dxdt,dxdt_hat_noisy)]):
109-
l2_error = np.linalg.norm(a - b)
110-
linf_error = np.max(np.abs(a - b))
157+
#l2_error = np.linalg.norm(a - b)
158+
#linf_error = np.max(np.abs(a - b))
159+
#print(f"({l2_error},{linf_error})", end=", ")
160+
#print(f"({int(np.ceil(np.log10(l2_error))) if l2_error > 0 else -25}, {int(np.ceil(np.log10(linf_error))) if linf_error > 0 else -25})", end=", ")
111161

112-
#print(f"({int(np.ceil(np.log10(l2_error))) if l2_error> 0 else -25}, {int(np.ceil(np.log10(linf_error))) if linf_error > 0 else -25})", end=", ")
113162
log_l2_bound, log_linf_bound = error_bounds[diff_method][i][j]
114163
assert np.linalg.norm(a - b) < 10**log_l2_bound
115164
assert np.max(np.abs(a - b)) < 10**log_linf_bound
116165
if 0 < np.linalg.norm(a - b) < 10**(log_l2_bound - 1) or 0 < np.max(np.abs(a - b)) < 10**(log_linf_bound - 1):
117-
print(f"Improvement detected for method {str(diff_method)}")
166+
print(f"Improvement detected for method {diff_method.__name__}")
118167

119-
if request.config.getoption("--plot"): # Get the plot flag from pytest configuration
120-
fig, axes = request.config.plots[diff_method]
168+
if request.config.getoption("--plot") and not isinstance(params, list): # Get the plot flag from pytest configuration
169+
fig, axes = request.config.plots[diff_method] # get the appropriate plot, set up by the store_plots fixture in conftest.py
121170
axes[i, 0].plot(t, f(t), label=r"$x(t)$")
122171
axes[i, 0].plot(t, x, 'C0+')
123172
axes[i, 0].plot(tt, df(tt), label=r"$\frac{dx(t)}{dt}$")

pynumdiff/tests/test_smooth_finite_difference.py

Lines changed: 0 additions & 78 deletions
This file was deleted.

0 commit comments

Comments
 (0)