@@ -264,7 +264,7 @@ def constant_jerk(x, dt, params=None, options=None, r=None, q=None, forwardbackw
264264 return rtsdiff (x , dt , 3 , np .log10 (q / r ), forwardbackward )
265265
266266
267- def robustdiff (x , dt , order , log_q , log_r , proc_huberM = 6 , meas_huberM = 1.345 ):
267+ def robustdiff (x , dt , order , log_q , log_r , proc_huberM = 6 , meas_huberM = 0 ):
268268 """Perform outlier-robust differentiation by solving the Maximum A Priori optimization problem:
269269 :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}))`,
270270 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
@@ -275,13 +275,12 @@ def robustdiff(x, dt, order, log_q, log_r, proc_huberM=6, meas_huberM=1.345):
275275 all inputs to Huber are normalized by noise level, :code:`q` or :code:`r`, :code:`M` is in units of standard deviation.
276276 In other words, this choice affects which portion of inputs are treated as outliers. For example, assuming Gaussian inliers,
277277 the portion beyond :math:`M\\ sigma` is :code:`outlier_portion = 2*(1 - scipy.stats.norm.cdf(M))`. The inverse of this is
278- :code:`M = scipy.stats.norm.ppf(1 - outlier_portion/2)`. By :code:`M=6`, Huber is essentially indistinguishable from the
279- 1/2-sum-of-squares case, :math:`\\ frac{1}{2}\\ |\\ cdot\\ |_2^2`, because the portion of examples beyond :math:`6\\ sigma` is
280- miniscule, about :code:`1.97e-9`, and the normalization constant of the Huber loss approaches 1 as :math:`M` increases.
281- (See :math:`c_2` `in section 6 <https://jmlr.org/papers/volume14/aravkin13a/aravkin13a.pdf>`_, missing a :math:`\\ sqrt{\\ cdot}`
282- term there, see p2700). Similarly, as :code:`M` approaches 0, Huber reduces to the :math:`\\ ell_1` norm case, because
283- :math:`c_2` approaches :math:`\\ frac{\\ sqrt{2}}{M}`, cancelling the :math:`M` multiplying :math:`|\\ cdot|` and leaving
284- behind :math:`\\ sqrt{2}`, the proper normalization.
278+ :code:`M = scipy.stats.norm.ppf(1 - outlier_portion/2)`. As :math:`M \\ to \\ infty`, Huber becomes the 1/2-sum-of-squares
279+ case, :math:`\\ frac{1}{2}\\ |\\ cdot\\ |_2^2`, and the normalization constant of the Huber loss approaches 1 as :math:`M`
280+ increases. (See :math:`c_2` `in section 6 <https://jmlr.org/papers/volume14/aravkin13a/aravkin13a.pdf>`_, missing a
281+ :math:`\\ sqrt{\\ cdot}` term there, see p2700). Similarly, as :code:`M` approaches 0, Huber reduces to the :math:`\\ ell_1`
282+ norm case, because :math:`c_2` approaches :math:`\\ frac{\\ sqrt{2}}{M}`, cancelling the :math:`M` multiplying :math:`|\\ cdot|`
283+ and leaving behind :math:`\\ sqrt{2}`, the proper normalization.
285284
286285 :param np.array[float] x: data series to differentiate
287286 :param float dt: step size
@@ -295,6 +294,9 @@ def robustdiff(x, dt, order, log_q, log_r, proc_huberM=6, meas_huberM=1.345):
295294 - **x_hat** -- estimated (smoothed) x
296295 - **dxdt_hat** -- estimated derivative of x
297296 """
297+ #q = 1e4 # I found q too small worsened condition number of the Q matrix, so fixing it at a biggish value
298+ #r = q/qr_ratio
299+
298300 A_c = np .diag (np .ones (order ), 1 ) # continuous-time A just has 1s on the first diagonal (where 0th is main diagonal)
299301 Q_c = np .zeros (A_c .shape ); Q_c [- 1 ,- 1 ] = 10 ** log_q # continuous-time uncertainty around the last derivative
300302 C = np .zeros ((1 , order + 1 )); C [0 ,0 ] = 1 # we measure only y = noisy x
@@ -310,19 +312,19 @@ def robustdiff(x, dt, order, log_q, log_r, proc_huberM=6, meas_huberM=1.345):
310312 return x_states [0 ], x_states [1 ]
311313
312314
313- def convex_smooth (y , A , Q , C , R , proc_huberM = 6 , meas_huberM = 1.345 ):
315+ def convex_smooth (y , A , Q , C , R , proc_huberM , meas_huberM ):
314316 """Solve the optimization problem for robust smoothing using CVXPY. Note this currently assumes constant dt
315- but could be extended to handle variable step sizes by finding discrete-time A and Q for requisite gaps as
316- in the :code:`kalman_filter` function.
317+ but could be extended to handle variable step sizes by finding discrete-time A and Q for requisite gaps.
317318
318319 :param np.array[float] y: measurements
319320 :param np.array A: discrete-time state transition matrix
320321 :param np.array Q: discrete-time process noise covariance matrix
321322 :param np.array C: measurement matrix
322323 :param np.array R: measurement noise covariance matrix
323- :param float huberM: radius where quadratic of Huber loss function turns linear. M=0 reduces to the :math:`\\ ell_1` norm.
324+ :param float proc_huberM: Huber loss parameter. :math:`M=0` reduces to :math:`\\ sqrt{2}\\ ell_1`.
325+ :param float meas_huberM: Huber loss parameter. :math:`M=\\ infty` reduces to :math:`\\ frac{1}{2}\\ ell_2^2`.
324326
325- :return: np.array -- state estimates (N x state_dim )
327+ :return: np.array -- state estimates (state_dim x N )
326328 """
327329 N = len (y )
328330 x_states = cvxpy .Variable ((A .shape [0 ], N )) # each column is [position, velocity, acceleration, ...] at step n
@@ -334,27 +336,19 @@ def huber_const(M): # from https://jmlr.org/papers/volume14/aravkin13a/aravkin13
334336
335337 # It is extremely important to run time that CVXPY expressions be in vectorized form
336338 proc_resids = np .linalg .inv (sqrtm (Q )) @ (x_states [:,1 :] - A @ x_states [:,:- 1 ]) # all Q^(-1/2)(x_n - A x_{n-1})
337- meas_resids = np .linalg .inv (sqrtm (R )) @ (y .reshape (1 ,- 1 ) - C @ x_states ) # all R^(-1/2)(y_n - C x_n)
339+ 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)
338340 # Process terms: sum of J(proc_resids)
339- objective = 0.5 * cvxpy .sum_squares (proc_resids ) if proc_huberM > ( 6 - 1e-3 ) \
340- else np .sqrt (2 )* cvxpy .sum (cvxpy .abs (proc_resids )) if proc_huberM < 1e-3 \
341- else huber_const (proc_huberM )* cvxpy .sum (cvxpy .huber (proc_resids , proc_huberM )) # 1/2 l2^2, l1, or Huber
341+ objective = 0.5 * cvxpy .sum_squares (proc_resids ) if huberM == float ( 'inf' ) \
342+ else np .sqrt (2 )* cvxpy .sum (cvxpy .abs (proc_resids )) if huberM < 1e-3 \
343+ else huber_const (huberM )* cvxpy .sum (cvxpy .huber (proc_resids , huberM )) # 1/2 l2^2, l1, or Huber
342344 # Measurement terms: sum of V(meas_resids)
343- objective += 0.5 * cvxpy .sum_squares (meas_resids ) if meas_huberM > ( 6 - 1e-3 ) \
344- else np .sqrt (2 )* cvxpy .sum (cvxpy .abs (meas_resids )) if meas_huberM < 1e-3 \
345- else huber_const (meas_huberM )* cvxpy .sum (cvxpy .huber (meas_resids , meas_huberM )) # CVXPY quirk: norm(, 1) != sum(abs()) for matrices
346-
345+ objective += 0.5 * cvxpy .sum_squares (meas_resids ) if huberM == float ( 'inf' ) \
346+ else np .sqrt (2 )* cvxpy .sum (cvxpy .abs (meas_resids )) if huberM < 1e-3 \
347+ else huber_const (huberM )* cvxpy .sum (cvxpy .huber (meas_resids , huberM )) # CVXPY quirk: norm(, 1) != sum(abs()) for matrices
348+
347349 problem = cvxpy .Problem (cvxpy .Minimize (objective ))
348- try :
349- before = time ()
350- problem .solve (solver = cvxpy .CLARABEL )
351- print (f"CLARABEL succeeded in { time () - before } s" )
352- except cvxpy .error .SolverError : pass
353- #print("CLARABEL failed with status {problem.status}. Returning NaNs.")
354- #return np.full((A.shape[0], N), np.nan)
355- # problem.solve(solver=cvxpy.SCS) # Alternatively, SCS is a lot slower but pretty bulletproof even with big condition numbers
356- if x_states .value is None : # There can be solver failure
357- #print(f"Convex solvers failed with status {problem.status}. Returning NaNs.")
358- return np .full ((A .shape [0 ], N ), np .nan )
350+ try : problem .solve (solver = cvxpy .CLARABEL ); print ("CLARABEL succeeded" )
351+ except cvxpy .error .SolverError : pass # Could try a different solver here, like SCS, but slows things down
359352
353+ if x_states .value is None : return np .full ((A .shape [0 ], N ), np .nan ) # There can be solver failure, even without exception
360354 return x_states .value
0 commit comments