@@ -259,7 +259,7 @@ def constant_jerk(x, dt, params=None, options=None, r=None, q=None, forwardbackw
259259 return rtsdiff (x , dt , 3 , np .log10 (q / r ), forwardbackward )
260260
261261
262- def robustdiff (x , dt , order , log_q , log_r , proc_huberM = 6 , meas_huberM = 0 ):
262+ def robustdiff (x , dt_or_t , order , log_q , log_r , proc_huberM = 6 , meas_huberM = 0 ):
263263 """Perform outlier-robust differentiation by solving the Maximum A Priori optimization problem:
264264 :math:`\\ text{argmin}_{\\ {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}))`,
265265 where :math:`A,Q,C,R` come from an assumed constant derivative model and :math:`V,J` are the :math:`\\ ell_1` norm or Huber
@@ -295,16 +295,31 @@ def robustdiff(x, dt, order, log_q, log_r, proc_huberM=6, meas_huberM=0):
295295 :return: - **x_hat** (np.array) -- estimated (smoothed) x
296296 - **dxdt_hat** (np.array) -- estimated derivative of x
297297 """
298+ equispaced = np .isscalar (dt_or_t )
299+ if not equispaced and len (x ) != len (dt_or_t ):
300+ raise ValueError ("If `dt_or_t` is given as array-like, must have same length as `x`." )
301+
298302 A_c = np .diag (np .ones (order ), 1 ) # continuous-time A just has 1s on the first diagonal (where 0th is main diagonal)
299303 Q_c = np .zeros (A_c .shape ); Q_c [- 1 ,- 1 ] = 10 ** log_q # continuous-time uncertainty around the last derivative
300304 C = np .zeros ((1 , order + 1 )); C [0 ,0 ] = 1 # we measure only y = noisy x
301305 R = np .array ([[10 ** log_r ]]) # 1 observed state, so this is 1x1
306+ M = np .block ([[A_c , Q_c ], [np .zeros (A_c .shape ), - A_c .T ]]) # exponentiate per step
302307
303- # convert to discrete time using matrix exponential
304- 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
305- A_d = eM [:order + 1 , :order + 1 ]
306- Q_d = eM [:order + 1 , order + 1 :] @ A_d .T
307- 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).
308+ if equispaced :
309+ # convert to discrete time using matrix exponential
310+ eM = expm (M * dt_or_t ) # Note this could handle variable dt, similar to rtsdiff
311+ A_d = eM [:order + 1 , :order + 1 ]
312+ Q_d = eM [:order + 1 , order + 1 :] @ A_d .T
313+ 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).
314+ else : # support variable step size for this function
315+ A_d = np .empty ((len (x )- 1 , order + 1 , order + 1 )) # stack all the evolution matrices
316+ Q_d = np .empty ((len (x )- 1 , order + 1 , order + 1 ))
317+
318+ for i , dt in enumerate (np .diff (dt_or_t )): # for each variable time step
319+ eM = expm (M * dt )
320+ A_d [i ] = eM [:order + 1 , :order + 1 ] # extract discrete time A matrix
321+ Q_d [i ] = eM [:order + 1 , order + 1 :] @ A_d [i ].T # extract discrete time Q matrix
322+ if np .linalg .cond (Q_d [i ]) > 1e12 : Q_d [i ] += np .eye (order + 1 )* 1e-12
308323
309324 x_states = convex_smooth (x , A_d , Q_d , C , R , proc_huberM = proc_huberM , meas_huberM = meas_huberM ) # outsource solution of the convex optimization problem
310325 return x_states [:,0 ], x_states [:,1 ]
@@ -327,12 +342,20 @@ def convex_smooth(y, A, Q, C, R, B=None, u=None, proc_huberM=6, meas_huberM=0):
327342 :return: (np.array) -- state estimates (state_dim x N)
328343 """
329344 N = len (y )
330- x_states = cvxpy .Variable ((A .shape [0 ], N )) # each column is [position, velocity, acceleration, ...] at step n
345+ state_dim = A .shape [- 1 ]
346+ x_states = cvxpy .Variable ((state_dim , N )) # each column is [position, velocity, acceleration, ...] at step n
331347 control = isinstance (B , np .ndarray ) and isinstance (u , np .ndarray ) # whether there is a control input
332348
333- # It is extremely important to run time that CVXPY expressions be in vectorized form
334- proc_resids = np .linalg .inv (sqrtm (Q )) @ (x_states [:,1 :] - A @ x_states [:,:- 1 ] - (0 if not control else B @ u [1 :].T )) # all Q^(-1/2)(x_n - (A x_{n-1} + B u_n))
335- meas_resids = np .linalg .inv (sqrtm (R )) @ (y .reshape (C .shape [0 ],- 1 ) - C @ x_states ) # all R^(-1/2)(y_n - C x_n)
349+ if A .ndim == 3 : # It is extremely important to runtime that CVXPY expressions be in vectorized form
350+ Ax = cvxpy .einsum ('nij,jn->in' , A , x_states [:, :- 1 ]) # multipy each A matrix by the corresponding x_states at that time step
351+ Q_inv_sqrts = np .array ([np .linalg .inv (sqrtm (Q [n ])) for n in range (N - 1 )]) # precompute Q^(-1/2) for each time step
352+ proc_resids = cvxpy .einsum ('nij,jn->in' , Q_inv_sqrts , x_states [:,1 :] - Ax - (0 if not control else B @ u [1 :].T ))
353+ else : # all Q^(-1/2)(x_n - (A x_{n-1} + B u_n))
354+ proc_resids = np .linalg .inv (sqrtm (Q )) @ (x_states [:,1 :] - A @ x_states [:,:- 1 ] - (0 if not control else B @ u [1 :].T ))
355+
356+ obs = ~ np .isnan (y ) # boolean mask of non-NaN observations
357+ meas_resids = np .linalg .inv (sqrtm (R )) @ (y [obs ].reshape (C .shape [0 ],- 1 ) - C @ x_states [:,obs ]) # all R^(-1/2)(y_n - C x_n)
358+
336359 # Process terms: sum of J(proc_resids)
337360 objective = 0.5 * cvxpy .sum_squares (proc_resids ) if proc_huberM == float ('inf' ) \
338361 else np .sqrt (2 )* cvxpy .sum (cvxpy .abs (proc_resids )) if proc_huberM < 1e-3 \
@@ -345,8 +368,8 @@ def convex_smooth(y, A, Q, C, R, B=None, u=None, proc_huberM=6, meas_huberM=0):
345368 # function https://www.cvxpy.org/api_reference/cvxpy.atoms.elementwise.html#huber, so correct with a factor of 0.5.
346369
347370 problem = cvxpy .Problem (cvxpy .Minimize (objective ))
348- try : problem .solve (solver = cvxpy .CLARABEL )
371+ try : problem .solve (solver = cvxpy .CLARABEL , canon_backend = cvxpy . SCIPY_CANON_BACKEND )
349372 except cvxpy .error .SolverError : pass # Could try another solver here, like SCS, but slows things down
350373
351- if x_states .value is None : return np .full ((N , A . shape [ 0 ] ), np .nan ) # There can be solver failure, even without error
374+ if x_states .value is None : return np .full ((N , state_dim ), np .nan ) # There can be solver failure, even without error
352375 return x_states .value .T
0 commit comments