Skip to content

Commit de68e28

Browse files
committed
added caching to optimization
1 parent a8af547 commit de68e28

File tree

2 files changed

+120
-105
lines changed

2 files changed

+120
-105
lines changed

pynumdiff/kalman_smooth/_kalman_smooth.py

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,11 @@ def constant_jerk(x, dt, params=None, options=None, r=None, q=None, forwardbackw
256256
return rtsdiff(x, dt, 3, q/r, forwardbackward)
257257

258258

259-
def robustdiff(x, dt, order, qr_ratio):
259+
def robustdiff(x, dt, order, qr_ratio, huberM):
260260
"""Perform robust differentiation using L1-norm optimization instead of L2-norm RTS smoothing.
261261
262262
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
263+
:math:`\\min_{\\{x_n\\}} \\sum_{n=0}^{N-1} V(R^(-1/2)(y_n - C x_n)) + sum_{n=1}^{N-1} J(Q^(-1/2)(x_n - A x_{n-1}))`
264264
265265
The L1 norm provides better robustness to outliers compared to the L2 norm used in standard
266266
Kalman smoothing. Uses CVXPY for convex optimization.
@@ -270,6 +270,7 @@ def robustdiff(x, dt, order, qr_ratio):
270270
:param int order: which derivative to stabilize in the constant-derivative model (1=velocity, 2=acceleration, 3=jerk)
271271
:param float qr_ratio: the process noise level of the divided by the measurement noise level, because the result is
272272
dependent on the relative rather than absolute size of :math:`q` and :math:`r`.
273+
:param float huberM: radius where quadratic of Huber loss function turns linear. M=0 reduces to the :math:`\\ell_1` norm.
273274
274275
:return: tuple[np.array, np.array] of\n
275276
- **x_hat** -- estimated (smoothed) x
@@ -286,51 +287,48 @@ def robustdiff(x, dt, order, qr_ratio):
286287
R = np.array([[r]]) # 1 observed state, so this is 1x1
287288

288289
# 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+
eM = expm(np.block([[A_c, Q_c], [np.zeros(A_c.shape), -A_c.T]]) * dt) # Note this could handle variable dt, similar to rtsdiff
290291
A_d = eM[:order+1, :order+1]
291292
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}")
293+
if np.linalg.cond(Q_d) > 1e12: Q_d += np.eye(order + 1)*1e-12 # for numerical stability with convex solver. Doesn't change answers appreciably (or at all).
295294

296-
# Solve the L1-norm optimization problem
297-
x_states = convex_smooth(x, A_d, Q_d, R, C)
295+
print(f"order={order}, qr_ratio={qr_ratio:.3e}, huberM={huberM}, cond(Q)={np.linalg.cond(Q_d):.3e}")
296+
x_states = convex_smooth(x, A_d, Q_d, R, C, huberM=huberM) # outsource solution of the convex optimization problem
298297
return x_states[:, 0], x_states[:, 1]
299298

300299

301-
def convex_smooth(y, A, Q, R, C, huberM=1):
302-
"""Solve the L1-norm optimization problem for robust smoothing using CVXPY.
300+
def convex_smooth(y, A, Q, R, C, huberM=0):
301+
"""Solve the optimization problem for robust smoothing using CVXPY. Note this currently assumes constant dt
302+
but could be extended to handle variable step sizes by finding discrete-time A and Q for requisite gaps.
303303
304304
:param np.array[float] y: measurements
305305
:param np.array A: discrete-time state transition matrix
306306
:param np.array Q: discrete-time process noise covariance matrix
307307
:param np.array R: measurement noise covariance matrix
308308
:param np.array C: measurement matrix
309309
:param float huberM: radius where quadratic of Huber loss function turns linear. M=0 reduces to the :math:`\\ell_1` norm.
310+
310311
:return: np.array -- state estimates (N x state_dim)
311312
"""
312313
N = len(y)
313314
x_states = cvxpy.Variable((N, A.shape[0])) # each row is [position, velocity, acceleration, ...] at step n
314315

315316
R_sqrt_inv = np.linalg.inv(sqrtm(R))
316317
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+
objective = cvxpy.sum([cvxpy.norm(R_sqrt_inv @ (y[n] - C @ x_states[n]), 1) if huberM < 1e-3
318319
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+
objective += cvxpy.sum([cvxpy.norm(Q_sqrt_inv @ (x_states[n] - A @ x_states[n-1]), 1) if huberM < 1e-3
320321
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
321322

322323
problem = cvxpy.Problem(cvxpy.Minimize(objective))
323324
try:
324325
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}")
326+
except cvxpy.error.SolverError:
327+
print(f"CLARABEL failed. Retrying with SCS.")
328+
problem.solve(solver=cvxpy.SCS) # SCS is a lot slower but pretty bulletproof even with big condition numbers
329+
330+
if x_states.value is None: # There is occasional solver failure with huber as opposed to 1-norm
331+
warn("Convex solvers failed with status {problem.status}. Returning NaNs.")
334332
return np.full((N, A.shape[0]), np.nan)
335333

336334
return x_states.value

0 commit comments

Comments
 (0)