Skip to content

Commit a8af547

Browse files
committed
added robustdiff to kalman_smooth, added tests for it, added it to notebooks 1 and 2a (2b yet to do), added it to optimization and played with it a long time, still toying, improved the way imports are done in various __init__.py with the else clause, which I didn't know about before
1 parent 942692a commit a8af547

8 files changed

Lines changed: 391 additions & 207 deletions

File tree

examples/1_basic_tutorial.ipynb

Lines changed: 167 additions & 131 deletions
Large diffs are not rendered by default.

examples/2a_optimizing_parameters_with_dxdt_known.ipynb

Lines changed: 101 additions & 63 deletions
Large diffs are not rendered by default.

pynumdiff/__init__.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,19 @@
22
"""
33
from ._version import __version__
44

5+
try: # cvxpy dependencies
6+
import cvxpy
7+
except ImportError:
8+
from warnings import warn
9+
warn("tvrdiff, robustdiff, and lineardiff not available due to lack of convex solver. To use those, install CVXPY.")
10+
else: # executes if try is successful
11+
from .total_variation_regularization import tvrdiff, velocity, acceleration, jerk, smooth_acceleration, jerk_sliding
12+
from .kalman_smooth import robustdiff, convex_smooth
13+
from .linear_model import lineardiff
14+
515
from .finite_difference import finitediff, first_order, second_order, fourth_order
616
from .smooth_finite_difference import meandiff, mediandiff, gaussiandiff, friedrichsdiff, butterdiff
717
from .polynomial_fit import splinediff, polydiff, savgoldiff
8-
from .total_variation_regularization import tvrdiff, velocity, acceleration, jerk, iterative_velocity, smooth_acceleration, jerk_sliding
9-
from .kalman_smooth import kalman_filter, rts_smooth, rtsdiff, constant_velocity, constant_acceleration, constant_jerk
1018
from .basis_fit import spectraldiff, rbfdiff
11-
from .linear_model import lineardiff
19+
from .total_variation_regularization import iterative_velocity
20+
from .kalman_smooth import kalman_filter, rts_smooth, rtsdiff, constant_velocity, constant_acceleration, constant_jerk
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1-
"""This module implements Kalman filters
1+
"""This module implements constant-derivative model-based smoothers based on Kalman filtering and its generalization.
22
"""
3+
try:
4+
import cvxpy
5+
except:
6+
from warnings import warn
7+
warn("CVXPY is not installed; robustdiff and l1_solve not available.")
8+
else: # runs if try was successful
9+
from ._kalman_smooth import robustdiff, convex_smooth
10+
311
from ._kalman_smooth import kalman_filter, rts_smooth, rtsdiff, constant_velocity, constant_acceleration, constant_jerk

pynumdiff/kalman_smooth/_kalman_smooth.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import numpy as np
22
from warnings import warn
3-
from scipy.linalg import expm
3+
from scipy.linalg import expm, sqrtm
4+
5+
try: import cvxpy
6+
except ImportError: pass
47

58

69
def kalman_filter(y, _t, xhat0, P0, A, Q, C, R, B=None, u=None, save_P=True):
@@ -251,3 +254,83 @@ def constant_jerk(x, dt, params=None, options=None, r=None, q=None, forwardbackw
251254
raise ValueError("`q` and `r` must be given.")
252255

253256
return rtsdiff(x, dt, 3, q/r, forwardbackward)
257+
258+
259+
def robustdiff(x, dt, order, qr_ratio):
260+
"""Perform robust differentiation using L1-norm optimization instead of L2-norm RTS smoothing.
261+
262+
This function solves the L1-normed MAP optimization problem for outlier-resistant differentiation:
263+
min_{x_n} sum_{n=0}^{N-1} ||R^(-1/2)(y_n - C x_n)||_1 + sum_{n=1}^{N-1} ||Q^(-1/2)(x_n - A x_{n-1})||_1
264+
265+
The L1 norm provides better robustness to outliers compared to the L2 norm used in standard
266+
Kalman smoothing. Uses CVXPY for convex optimization.
267+
268+
:param np.array[float] x: data series to differentiate
269+
:param float dt: step size
270+
:param int order: which derivative to stabilize in the constant-derivative model (1=velocity, 2=acceleration, 3=jerk)
271+
:param float qr_ratio: the process noise level of the divided by the measurement noise level, because the result is
272+
dependent on the relative rather than absolute size of :math:`q` and :math:`r`.
273+
274+
:return: tuple[np.array, np.array] of\n
275+
- **x_hat** -- estimated (smoothed) x
276+
- **dxdt_hat** -- estimated derivative of x
277+
"""
278+
#q = 10**int(np.log10(qr_ratio)) # even-ish split of the powers across 0
279+
#r = q/qr_ratio
280+
q = 1e4
281+
r = q/qr_ratio
282+
283+
A_c = np.diag(np.ones(order), 1) # continuous-time A just has 1s on the first diagonal (where 0th is main diagonal)
284+
Q_c = np.zeros(A_c.shape); Q_c[-1,-1] = q # continuous-time uncertainty around the last derivative
285+
C = np.zeros((1, order+1)); C[0,0] = 1 # we measure only y = noisy x
286+
R = np.array([[r]]) # 1 observed state, so this is 1x1
287+
288+
# convert to discrete time using matrix exponential
289+
eM = expm(np.block([[A_c, Q_c], [np.zeros(A_c.shape), -A_c.T]]) * dt)
290+
A_d = eM[:order+1, :order+1]
291+
Q_d = eM[:order+1, order+1:] @ A_d.T
292+
if np.linalg.cond(Q_d) > 1e12: Q_d += np.eye(order + 1)*1e-15 # for numerical stability with convex solver. Doesn't change answers appreciably (or at all).
293+
294+
print(f"order = {order}, qr_ratio = {qr_ratio:.3e}, (q,r) = {(q,r)}, cond(Q_d) = {np.linalg.cond(Q_d):.3e}")
295+
296+
# Solve the L1-norm optimization problem
297+
x_states = convex_smooth(x, A_d, Q_d, R, C)
298+
return x_states[:, 0], x_states[:, 1]
299+
300+
301+
def convex_smooth(y, A, Q, R, C, huberM=1):
302+
"""Solve the L1-norm optimization problem for robust smoothing using CVXPY.
303+
304+
:param np.array[float] y: measurements
305+
:param np.array A: discrete-time state transition matrix
306+
:param np.array Q: discrete-time process noise covariance matrix
307+
:param np.array R: measurement noise covariance matrix
308+
:param np.array C: measurement matrix
309+
:param float huberM: radius where quadratic of Huber loss function turns linear. M=0 reduces to the :math:`\\ell_1` norm.
310+
:return: np.array -- state estimates (N x state_dim)
311+
"""
312+
N = len(y)
313+
x_states = cvxpy.Variable((N, A.shape[0])) # each row is [position, velocity, acceleration, ...] at step n
314+
315+
R_sqrt_inv = np.linalg.inv(sqrtm(R))
316+
Q_sqrt_inv = np.linalg.inv(sqrtm(Q))
317+
objective = cvxpy.sum([cvxpy.norm(R_sqrt_inv @ (y[n] - C @ x_states[n]), 1) if huberM == 0
318+
else cvxpy.sum(cvxpy.huber(R_sqrt_inv @ (y[n] - C @ x_states[n]), huberM)) for n in range(N)]) # Measurement terms: sum of |R^(-1/2)(y_n - C x_n)|_1
319+
objective += cvxpy.sum([cvxpy.norm(Q_sqrt_inv @ (x_states[n] - A @ x_states[n-1]), 1) if huberM == 0
320+
else cvxpy.sum(cvxpy.huber(Q_sqrt_inv @ (x_states[n] - A @ x_states[n-1]), huberM)) for n in range(1, N)]) # Process terms: sum of |Q^(-1/2)(x_n - A x_{n-1})|_1
321+
322+
problem = cvxpy.Problem(cvxpy.Minimize(objective))
323+
try:
324+
problem.solve(solver=cvxpy.CLARABEL)
325+
except cvxpy.error.SolverError as e1:
326+
print(f"CLARABEL failed ({e1}). Retrying with SCS.")
327+
try:
328+
problem.solve(solver=cvxpy.SCS) # SCS is a lot slower but pretty bulletproof even with big condition numbers
329+
except cvxpy.error.SolverError as e2:
330+
print(f"SCS failed ({e2}). Returning NaNs.")
331+
332+
if x_states.value is None:
333+
print(f"Solver returned None. Status: {problem.status}")
334+
return np.full((N, A.shape[0]), np.nan)
335+
336+
return x_states.value

pynumdiff/optimize/_optimize.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ..polynomial_fit import polydiff, savgoldiff, splinediff
1515
from ..basis_fit import spectraldiff, rbfdiff
1616
from ..total_variation_regularization import tvrdiff, velocity, acceleration, jerk, iterative_velocity, smooth_acceleration, jerk_sliding
17-
from ..kalman_smooth import rtsdiff, constant_velocity, constant_acceleration, constant_jerk
17+
from ..kalman_smooth import rtsdiff, constant_velocity, constant_acceleration, constant_jerk, robustdiff
1818

1919

2020
# Map from method -> (search_space, bounds_low_hi)
@@ -88,7 +88,10 @@
8888
'q': [1e-8, 1e-4, 1e-1, 1e1, 1e4, 1e8],
8989
'r': [1e-8, 1e-4, 1e-1, 1e1, 1e4, 1e8]},
9090
{'q': (1e-10, 1e10),
91-
'r': (1e-10, 1e10)})
91+
'r': (1e-10, 1e10)}),
92+
robustdiff: ({'order': {1, 2, 3}, #categorical
93+
'qr_ratio': [10**k for k in range(-1, 18, 3)]},
94+
{'qr_ratio': [1e-1, 1e18]})
9295
}
9396
for method in [second_order, fourth_order]:
9497
method_params_and_bounds[method] = method_params_and_bounds[first_order]
@@ -246,9 +249,9 @@ def suggest_method(x, dt, dxdt_truth=None, cutoff_frequency=None):
246249
spectraldiff, rbfdiff, splinediff, polydiff, savgoldiff, rtsdiff]
247250
try: # optionally skip some methods
248251
import cvxpy
249-
methods += [tvrdiff, smooth_acceleration]
252+
methods += [tvrdiff, smooth_acceleration, robustdiff]
250253
except ImportError:
251-
warn("CVXPY not installed, skipping tvrdiff and smooth_acceleration")
254+
warn("CVXPY not installed, skipping tvrdiff, smooth_acceleration, and robustdiff")
252255

253256
best_value = float('inf') # core loop
254257
for func in tqdm(methods):

pynumdiff/tests/test_diff_methods.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ..basis_fit import spectraldiff, rbfdiff
88
from ..polynomial_fit import polydiff, savgoldiff, splinediff
99
from ..total_variation_regularization import velocity, acceleration, jerk, iterative_velocity, smooth_acceleration, jerk_sliding
10-
from ..kalman_smooth import rtsdiff, constant_velocity, constant_acceleration, constant_jerk
10+
from ..kalman_smooth import rtsdiff, constant_velocity, constant_acceleration, constant_jerk, robustdiff
1111
from ..smooth_finite_difference import mediandiff, meandiff, gaussiandiff, friedrichsdiff, butterdiff
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)
@@ -59,6 +59,7 @@ def spline_irreg_step(*args, **kwargs): return splinediff(*args, **kwargs)
5959
(lineardiff, {'order':3, 'gamma':5, 'window_size':11, 'solver':'CLARABEL'}), (lineardiff, [3, 5, 11], {'solver':'CLARABEL'}),
6060
(rbfdiff, {'sigma':0.5, 'lmbd':0.001})
6161
]
62+
diff_methods_and_params = [(robustdiff, {'order':3, 'qr_ratio':1e6})]
6263

6364
# All the testing methodology follows the exact same pattern; the only thing that changes is the
6465
# closeness to the right answer various methods achieve with the given parameterizations and random seed.
@@ -210,6 +211,12 @@ def spline_irreg_step(*args, **kwargs): return splinediff(*args, **kwargs)
210211
[(-2, -3), (0, 0), (0, -1), (1, 1)],
211212
[(-1, -2), (1, 1), (0, -1), (1, 1)],
212213
[(0, 0), (3, 3), (0, 0), (3, 3)]],
214+
robustdiff: [[(-25, -25), (-15, -15), (0, -1), (0, 0)],
215+
[(-14, -14), (-13, -13), (0, -1), (0, 0)],
216+
[(-14, -14), (-13, -13), (0, -1), (0, 0)],
217+
[(-1, -1), (0, 0), (0, -1), (1, 0)],
218+
[(0, 0), (1, 1), (0, 0), (1, 1)],
219+
[(1, 1), (3, 3), (1, 1), (3, 3)]],
213220
spectraldiff: [[(-9, -10), (-14, -15), (-1, -1), (0, 0)],
214221
[(0, 0), (1, 1), (0, 0), (1, 1)],
215222
[(1, 1), (1, 1), (1, 1), (1, 1)],

pynumdiff/total_variation_regularization/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
"""
33
try:
44
import cvxpy
5-
from ._total_variation_regularization import tvrdiff, velocity, acceleration, jerk, jerk_sliding, smooth_acceleration
65
except:
76
from warnings import warn
87
warn("Limited Total Variation Regularization Support Detected! CVXPY is not installed. " +
98
"Many Total Variation Methods require CVXPY including: velocity, acceleration, jerk, jerk_sliding, smooth_acceleration. " +
10-
"Please install CVXPY to use these methods. Recommended to also install MOSEK and obtain a MOSEK license. " +
11-
"You can still use: total_variation_regularization.iterative_velocity.")
9+
"Please install CVXPY to use these methods. You can still use: total_variation_regularization.iterative_velocity.")
10+
else: # executes if try is successful
11+
from ._total_variation_regularization import tvrdiff, velocity, acceleration, jerk, jerk_sliding, smooth_acceleration
1212

1313
from ._total_variation_regularization import iterative_velocity

0 commit comments

Comments
 (0)