@@ -37,9 +37,9 @@ def kalman_filter(y, xhat0, P0, A, Q, C, R, B=None, u=None, save_P=True):
3737 m = xhat0 .shape [0 ] # dimension of the state
3838 xhat_post = np .empty ((N ,m ))
3939 if save_P :
40- xhat_pre = np .empty (( N , m ) ) # _pre = a priori predictions based on only past information
40+ xhat_pre = np .empty_like ( xhat_post ) # _pre = a priori predictions based on only past information
4141 P_pre = np .empty ((N ,m ,m )) # _post = a posteriori combinations of all information available at a step
42- P_post = np .empty (( N , m , m ) )
42+ P_post = np .empty_like ( P_pre )
4343
4444 control = isinstance (B , np .ndarray ) and isinstance (u , np .ndarray ) # whether there is a control input
4545 if A .ndim == 2 : An , Qn , Bn = A , Q , B # single matrices, assign once outside the loop
@@ -113,8 +113,8 @@ def rtsdiff(x, dt_or_t, order, log_qr_ratio, forwardbackward, axis=0):
113113 :return: - **x_hat** (np.array) -- estimated (smoothed) x, same shape as input :code:`x`
114114 - **dxdt_hat** (np.array) -- estimated derivative of x, same shape as input :code:`x`
115115 """
116- x = np . moveaxis ( np . asarray ( x ), axis , 0 ) # move axis of differentiation to standard position
117- if not np .isscalar (dt_or_t ) and x . shape [ 0 ] != len (dt_or_t ):
116+ N = x . shape [ axis ]
117+ if not np .isscalar (dt_or_t ) and N != len (dt_or_t ):
118118 raise ValueError ("If `dt_or_t` is given as array-like, must have same length as x along `axis`." )
119119
120120 q = 10 ** int (log_qr_ratio / 2 ) # even-ish split of the powers across 0
@@ -132,25 +132,25 @@ def rtsdiff(x, dt_or_t, order, log_qr_ratio, forwardbackward, axis=0):
132132 Q_d = eM [:order + 1 , order + 1 :] @ A_d .T
133133 if forwardbackward : A_d_bwd = np .linalg .inv (A_d )
134134 else :
135- A_d = np .empty ((len ( x ) - 1 , order + 1 , order + 1 ))
135+ A_d = np .empty ((N - 1 , order + 1 , order + 1 ))
136136 Q_d = np .empty_like (A_d )
137- for i , dt in enumerate (np .diff (dt_or_t )):
137+ for n , dt in enumerate (np .diff (dt_or_t )):
138138 eM = expm (M * dt )
139- A_d [i ] = eM [:order + 1 , :order + 1 ]
140- Q_d [i ] = eM [:order + 1 , order + 1 :] @ A_d [i ].T
139+ A_d [n ] = eM [:order + 1 , :order + 1 ]
140+ Q_d [n ] = eM [:order + 1 , order + 1 :] @ A_d [n ].T
141141 if forwardbackward : A_d_bwd = np .linalg .inv (A_d [::- 1 ]) # properly broadcasts, taking inv of each stacked 2D array
142142
143143 x_hat = np .empty_like (x ); dxdt_hat = np .empty_like (x )
144- if forwardbackward : w = np .linspace (0 , 1 , len ( x ) ) # weights used to combine forward and backward results
144+ if forwardbackward : w = np .linspace (0 , 1 , N ) # weights used to combine forward and backward results
145145
146- for vec_idx in np .ndindex (x .shape [1 :]):
147- s = (slice (None ),) + vec_idx # for indexing the the vector we wish to differentiate
146+ for vec_idx in np .ndindex (x .shape [: axis ] + x . shape [ axis + 1 :]): # works properly for 1D case too
147+ s = vec_idx [: axis ] + (slice (None ),) + vec_idx [ axis :] # for indexing the vector we wish to differentiate
148148 xhat0 = np .zeros (order + 1 ); xhat0 [0 ] = x [s ][0 ] if not np .isnan (x [s ][0 ]) else 0 # The first estimate is the first seen state. See #110
149149
150150 xhat_pre , xhat_post , P_pre , P_post = kalman_filter (x [s ], xhat0 , P0 , A_d , Q_d , C , R )
151151 xhat_smooth = rts_smooth (A_d , xhat_pre , xhat_post , P_pre , P_post , compute_P_smooth = False )
152- x_hat [s ] = xhat_smooth [:, 0 ] # first dimension is time, so slice first element at all times
153- dxdt_hat [s ] = xhat_smooth [:, 1 ]
152+ x_hat [s ] = xhat_smooth [:,0 ] # first dimension is time, so slice first and second states at all times
153+ dxdt_hat [s ] = xhat_smooth [:,1 ]
154154
155155 if forwardbackward :
156156 xhat0 [0 ] = x [s ][- 1 ] if not np .isnan (x [s ][- 1 ]) else 0
@@ -161,7 +161,7 @@ def rtsdiff(x, dt_or_t, order, log_qr_ratio, forwardbackward, axis=0):
161161 x_hat [s ] = x_hat [s ] * w + xhat_smooth [:, 0 ][::- 1 ] * (1 - w )
162162 dxdt_hat [s ] = dxdt_hat [s ] * w + xhat_smooth [:, 1 ][::- 1 ] * (1 - w )
163163
164- return np . moveaxis ( x_hat , 0 , axis ), np . moveaxis ( dxdt_hat , 0 , axis )
164+ return x_hat , dxdt_hat
165165
166166
167167def constant_velocity (x , dt , params = None , options = None , r = None , q = None , forwardbackward = True ):
@@ -254,7 +254,7 @@ def constant_jerk(x, dt, params=None, options=None, r=None, q=None, forwardbackw
254254 return rtsdiff (x , dt , 3 , np .log10 (q / r ), forwardbackward )
255255
256256
257- def robustdiff (x , dt_or_t , order , log_q , log_r , proc_huberM = 6 , meas_huberM = 0 ):
257+ def robustdiff (x , dt_or_t , order , log_q , log_r , proc_huberM = 6 , meas_huberM = 0 , axis = 0 ):
258258 """Perform outlier-robust differentiation by solving the Maximum A Priori optimization problem:
259259 :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_{n-1}^{-1/2}(x_n - A_{n-1} x_{n-1}))`,
260260 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
@@ -287,38 +287,42 @@ def robustdiff(x, dt_or_t, order, log_q, log_r, proc_huberM=6, meas_huberM=0):
287287 :param float log_r: base 10 logarithm of measurement noise variance, so :code:`r = 10**log_r`
288288 :param float proc_huberM: quadratic-to-linear transition point for process loss
289289 :param float meas_huberM: quadratic-to-linear transition point for measurement loss
290+ :param int axis: data dimension along which differentiation is performed
290291
291- :return: - **x_hat** (np.array) -- estimated (smoothed) x
292- - **dxdt_hat** (np.array) -- estimated derivative of x
292+ :return: - **x_hat** (np.array) -- estimated (smoothed) x, same shape as input :code:`x`
293+ - **dxdt_hat** (np.array) -- estimated derivative of x, same shape as input :code:`x`
293294 """
294- equispaced = np . isscalar ( dt_or_t )
295- if not equispaced and len ( x ) != len (dt_or_t ):
296- raise ValueError ("If `dt_or_t` is given as array-like, must have same length as `x`." )
295+ N = x . shape [ axis ]
296+ if not np . isscalar ( dt_or_t ) and N != len (dt_or_t ):
297+ raise ValueError ("If `dt_or_t` is given as array-like, must have same length as `x` along `axis` ." )
297298
298299 A_c = np .diag (np .ones (order ), 1 ) # continuous-time A just has 1s on the first diagonal (where 0th is main diagonal)
299300 Q_c = np .zeros (A_c .shape ); Q_c [- 1 ,- 1 ] = 10 ** log_q # continuous-time uncertainty around the last derivative
300301 C = np .zeros ((1 , order + 1 )); C [0 ,0 ] = 1 # we measure only y = noisy x
301302 R = np .array ([[10 ** log_r ]]) # 1 observed state, so this is 1x1
302303 M = np .block ([[A_c , Q_c ], [np .zeros (A_c .shape ), - A_c .T ]]) # exponentiate per step
303304
304- if equispaced :
305- # convert to discrete time using matrix exponential
306- eM = expm (M * dt_or_t ) # Note this could handle variable dt, similar to rtsdiff
305+ if np .isscalar (dt_or_t ): # convert to discrete time using matrix exponential
306+ eM = expm (M * dt_or_t )
307307 A_d = eM [:order + 1 , :order + 1 ]
308308 Q_d = eM [:order + 1 , order + 1 :] @ A_d .T
309- 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).
309+ 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).
310310 else : # support variable step size for this function
311- A_d = np .empty ((len (x )- 1 , order + 1 , order + 1 )) # stack all the evolution matrices
312- Q_d = np .empty ((len (x )- 1 , order + 1 , order + 1 ))
313-
314- for n ,dt in enumerate (np .diff (dt_or_t )): # for each variable time step
311+ A_d = np .empty ((N - 1 , order + 1 , order + 1 ))
312+ Q_d = np .empty_like (A_d )
313+ for n ,dt in enumerate (np .diff (dt_or_t )):
315314 eM = expm (M * dt )
316315 A_d [n ] = eM [:order + 1 , :order + 1 ] # extract discrete time A matrix
317316 Q_d [n ] = eM [:order + 1 , order + 1 :] @ A_d [n ].T # extract discrete time Q matrix
318- if np .linalg .cond (Q_d [n ]) > 1e12 : Q_d [n ] += np .eye (order + 1 )* 1e-12
317+ if np .linalg .cond (Q_d [n ]) > 1e12 : Q_d [n ] += np .eye (order + 1 )* 1e-12
318+
319+ x_hat = np .empty_like (x ); dxdt_hat = np .empty_like (x )
320+ for vec_idx in np .ndindex (x .shape [:axis ] + x .shape [axis + 1 :]): # works properly for 1D case too
321+ s = vec_idx [:axis ] + (slice (None ),) + vec_idx [axis :]
322+ x_states = convex_smooth (x [s ], A_d , Q_d , C , R , proc_huberM = proc_huberM , meas_huberM = meas_huberM ) # outsource solution of the convex optimization problem
323+ x_hat [s ] = x_states [:,0 ]; dxdt_hat [s ] = x_states [:,1 ]
319324
320- 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
321- return x_states [:,0 ], x_states [:,1 ]
325+ return x_hat , dxdt_hat
322326
323327
324328def convex_smooth (y , A , Q , C , R , B = None , u = None , proc_huberM = 6 , meas_huberM = 0 ):
0 commit comments