@@ -9,8 +9,8 @@ def kalman_filter(y, _t, xhat0, P0, A, Q, C, R, B=None, u=None, save_P=True):
99 :param np.array y: series of measurements, stacked along axis 0.
1010 :param float or array[float] _t: This function supports variable step size. This parameter is either the constant
1111 dt if given as a single float, or data locations if given as an array of same length as :code:`x`.
12- :param np.array xhat0: initial estimate of state (often 0) one step before the first measurement
13- :param np.array P0: initial guess of state covariance (often identity matrix) before the first measurement
12+ :param np.array xhat0: a priori guess of initial state at the time of the first measurement
13+ :param np.array P0: a priori guess of state covariance at the time of first measurement (often identity matrix)
1414 :param np.array A: state transition matrix, in discrete time if constant dt, in continuous time if variable dt
1515 :param np.array Q: noise covariance matrix, in discrete time if constant dt, in continuous time if variable dt
1616 :param np.array C: measurement matrix
@@ -27,44 +27,51 @@ def kalman_filter(y, _t, xhat0, P0, A, Q, C, R, B=None, u=None, save_P=True):
2727 - **P_post** -- a posteriori estimates of P
2828 if :code:`save_P` is :code:`True` else only **xhat_post** to save memory
2929 """
30- # _pre variables are a priori predictions based on only past information
31- # _post variables are a posteriori combinations of all information available at the current step
32- xhat_post = []
33- if save_P : xhat_pre = []; P_pre = []; P_post = []
34- xhat = xhat0
35- P = P0
36-
37- equispaced = isinstance (_t , (float , int )) # I'd rather not call isinstance in the loop
30+ xhat_post = np .empty ((len (y ), * xhat0 .shape ))
31+ if save_P :
32+ xhat_pre = np .empty ((len (y ), * xhat0 .shape )) # _pre = a priori predictions based on only past information
33+ P_pre = np .empty ((len (y ), * P0 .shape )) # _post = a posteriori combinations of all information available at a step
34+ P_post = np .empty ((len (y ), * P0 .shape ))
35+ # determine some things ahead of the loop
36+ equispaced = np .isscalar (_t )
3837 control = isinstance (B , np .ndarray ) and isinstance (B , np .ndarray ) # whether there is a control input
38+ if equispaced :
39+ An , Qn , Bn = A , Q , B # in this case only need to assign once
40+ else :
41+ M = np .block ([[A , Q ],[numpy .zeros (A .shape ), - A .T ]]) # If variable dt, we'll exponentiate this a bunch
42+ if control : Mc = np .block ([[A , B ],[np .zeros ((A .shape [0 ], 2 * A .shape [1 ]))]])
43+
3944 for n in range (y .shape [0 ]):
40- if not equispaced :
41- M = expm (np .block ([[A , Q ],[numpy .zeros (A .shape ), - A .T ]]) * (_t [n ] - _t [n - 1 ])) # form discrete-time matrices TODO doesn't work at n=0
42- An = M [:order + 1 ,:order + 1 ] # upper left block
43- Qn = M [:order + 1 ,order + 1 :] @ An .T # upper right block
44- if control :
45- M = expm (np .block ([[A , B ], [np .zeros ((A .shape [0 ], 2 * A .shape [1 ]))]]))
46- Bn = M [:order + 1 ,order + 1 :] # upper right block
47- else : # matrices should already be discrete time
48- An , Qn , Bn = A , Q , B
49-
50- xhat_ = An @ xhat + Bn @ u if control else An @ xhat # ending underscores denote an a priori prediction
51- P_ = An @ P @ An .T + Qn # the dense matrix multiplies here are the most expensive step
45+ if n == 0 : # first iteration is a special case, involving less work
46+ xhat_ = xhat0
47+ P_ = P0
48+ else :
49+ if not equispaced :
50+ dt = _t [n ] - _t [n - 1 ]
51+ eM = expm (M * dt ) # form discrete-time matrices TODO doesn't work at n=0
52+ An = eM [:order + 1 ,:order + 1 ] # upper left block
53+ Qn = eM [:order + 1 ,order + 1 :] @ An .T # upper right block
54+ if dt < 0 : Qn = np .abs (Qn ) # eigenvalues go negative if reverse direction, but noise shouldn't shrink
55+ if control :
56+ eM = expm (Mc * dt )
57+ Bn = eM [:order + 1 ,order + 1 :] # upper right block
58+ xhat_ = An @ xhat + Bn @ u if control else An @ xhat # ending underscores denote an a priori prediction
59+ P_ = An @ P @ An .T + Qn # the dense matrix multiplies here are the most expensive step
60+
5261 xhat = xhat_ .copy () # copies, lest modifications to these variables change the a priori estimates. See #122
5362 P = P_ .copy ()
5463 if not np .isnan (y [n ]): # handle missing data
5564 K = P_ @ C .T @ np .linalg .inv (C @ P_ @ C .T + R )
5665 xhat += K @ (y [n ] - C @ xhat_ )
5766 P -= K @ C @ P_
5867 # the [n]th index of pre variables holds _{n|n-1} info; the [n]th index of post variables holds _{n|n} info
59- xhat_post . append ( xhat )
60- if save_P : xhat_pre . append ( xhat_ ) ; P_pre . append ( P_ ) ; P_post . append ( P )
68+ xhat_post [ n ] = xhat
69+ if save_P : xhat_pre [ n ] = xhat_ ; P_pre [ n ] = P_ ; P_post [ n ] = P
6170
62- xhat_post = np .stack (xhat_post , axis = 0 )
63- return xhat_post if not save_P else (np .stack (xhat_pre , axis = 0 ), xhat_post ,
64- np .stack (P_pre , axis = 0 ), np .stack (P_post , axis = 0 ))
71+ return xhat_post if not save_P else (xhat_pre , xhat_post , P_pre , P_post )
6572
6673
67- def rts_smooth (_t , A , xhat_pre , xhat_post , P_pre , P_post , compute_P_smooth = False ):
74+ def rts_smooth (_t , A , xhat_pre , xhat_post , P_pre , P_post , compute_P_smooth = True ):
6875 """Perform Rauch-Tung-Striebel smoothing, using information from forward Kalman filtering.
6976
7077 :param float or array[float] _t: This function supports variable step size. This parameter is either the constant
@@ -84,9 +91,10 @@ def rts_smooth(_t, A, xhat_pre, xhat_post, P_pre, P_post, compute_P_smooth=False
8491 xhat_smooth = np .empty (xhat_post .shape ); xhat_smooth [- 1 ] = xhat_post [- 1 ] # preallocate arrays
8592 if compute_P_smooth : P_smooth = np .empty (P_post .shape ); P_smooth [- 1 ] = P_post [- 1 ]
8693
87- equispaced = isinstance (_t , (int , float )) # I'd rather not call isinstance in a loop when it's avoidable
94+ equispaced = np .isscalar (_t ) # I'd rather not call isinstance in a loop when it's avoidable
95+ if equispaced : An = A # in this case only assign once, outside the loop
8896 for n in range (xhat_pre .shape [0 ]- 2 , - 1 , - 1 ):
89- An = A if equispaced else expm (A * (_t [n + 1 ] - _t [n ])) # state transition from n to n+1, per the paper
97+ if not equispaced : An = expm (A * (_t [n + 1 ] - _t [n ])) # state transition from n to n+1, per the paper
9098 C_RTS = P_post [n ] @ An .T @ np .linalg .inv (P_pre [n + 1 ]) # the [n+1]th index holds _{n+1|n} info
9199 xhat_smooth [n ] = xhat_post [n ] + C_RTS @ (xhat_smooth [n + 1 ] - xhat_pre [n + 1 ]) # The original authors use C, not to be confused
92100 if compute_P_smooth : P_smooth [n ] = P_post [n ] + C_RTS @ (P_smooth [n + 1 ] - P_pre [n + 1 ]) @ C_RTS .T # with the measurement matrix
@@ -95,8 +103,8 @@ def rts_smooth(_t, A, xhat_pre, xhat_post, P_pre, P_post, compute_P_smooth=False
95103
96104
97105def rtsdiff (x , _t , order , qr_ratio , forwardbackward ):
98- """Perform Rauch-Tung-Striebel smoothing with a naive constant derivative model. Other constant derivative
99- methods in this module call this function.
106+ """Perform Rauch-Tung-Striebel smoothing with a naive constant derivative model. Makes use of :code:`kalman_filter`
107+ and :code:`rts_smooth`, which are made public. Other constant derivative methods in this module call this function.
100108
101109 :param np.array[float] x: data series to differentiate
102110 :param float or array[float] _t: This function supports variable step size. This parameter is either the constant
@@ -112,7 +120,7 @@ def rtsdiff(x, _t, order, qr_ratio, forwardbackward):
112120 - **x_hat** -- estimated (smoothed) x
113121 - **dxdt_hat** -- estimated derivative of x
114122 """
115- if isinstance ( _t , ( np .ndarray , list ) ) and len (x ) != len (_t ):
123+ if not np .isscalar ( _t ) and len (x ) != len (_t ):
116124 raise ValueError ("If `_t` is given as array-like, must have same length as `x`." )
117125
118126 q = 10 ** int (np .log10 (qr_ratio )/ 2 ) # even-ish split of the powers across 0
@@ -125,36 +133,26 @@ def rtsdiff(x, _t, order, qr_ratio, forwardbackward):
125133 P0 = 100 * np .eye (order + 1 ) # See #110 for why this choice of P0
126134 xhat0 = np .zeros (A .shape [0 ]); xhat0 [0 ] = x [0 ] # The first estimate is the first seen state. See #110
127135
128- if isinstance (_t , ( float , int ) ):
129- M = expm (np .block ([[A , Q ],[numpy .zeros (A .shape ), - A .T ]]) * _t ) # form discrete-time versions
130- A = M [:order + 1 ,:order + 1 ]
131- Q = M [:order + 1 ,order + 1 :] @ A .T
136+ if np . isscalar (_t ):
137+ eM = expm (np .block ([[A , Q ],[np .zeros (A .shape ), - A .T ]]) * _t ) # form discrete-time versions
138+ A = eM [:order + 1 ,:order + 1 ]
139+ Q = eM [:order + 1 ,order + 1 :] @ A .T
132140
133141 xhat_pre , xhat_post , P_pre , P_post = kalman_filter (x , _t , xhat0 , P0 , A , Q , C , R ) # noisy x are the "y" in Kalman-land
134- xhat_smooth = rts_smooth (_t , A , xhat_pre , xhat_post , P_pre , P_post ) # not doing anything with P_smooth
142+ xhat_smooth = rts_smooth (_t , A , xhat_pre , xhat_post , P_pre , P_post , compute_P_smooth = False )
135143 x_hat_forward = xhat_smooth [:, 0 ] # first dimension is time, so slice first element at all times
136144 dxdt_hat_forward = xhat_smooth [:, 1 ]
137145
138146 if not forwardbackward : # bounce out here if not doing the same in reverse and then combining
139147 return x_hat_forward , dxdt_hat_forward
140148
141- # for backward case, if discrete time invert the matrix, and for continuous time reverse _t, because then dts will go negative
142149 xhat0 [0 ] = x [- 1 ] # starting from the other end of the signal
143150
144- ### TODO fix between here and next hashes
145- if isinstance (_t , (float , int )):
146- M = expm (np .block ([[A , Q ],[numpy .zeros (A .shape ), - A .T ]]) * - _t ) # form discrete-time versions
147- A = M [:order + 1 ,:order + 1 ] # inverse of A
148- Q = M [:order + 1 ,order + 1 :] @ A .T #
149-
150- Ainv = np .linalg .inv (A_c ) # dynamics are inverted, same as expm() with negative dt
151- _t_rev = _t [::- 1 ] _t
151+ if np .isscalar (_t ): A = np .linalg .inv (A ) # discrete time dynamics are just the inverse
152+ else : _t = _t [::- 1 ] # in continuous time, reverse the time vector so dts go negative
152153
153- xhat_pre , xhat_post , P_pre , P_post = _kalman_forward_filter (x [::- 1 ], _t [::- 1 ], xhat0 , P0 , A , Q , C , R ) if \
154- isinstance (_t , (np .ndarray , list )) else _kalman_forward_filter (x [::- 1 ], _t , xhat0 , P0 , np .linalg .inv (A ), Q , C , R )
155- xhat_smooth = _kalman_backward_smooth (_t_rev , A_c , xhat_pre , xhat_post , P_pre , P_post )
156- ###
157-
154+ xhat_pre , xhat_post , P_pre , P_post = kalman_filter (x [::- 1 ], _t , xhat0 , P0 , A , Q , C , R )
155+ xhat_smooth = rts_smooth (_t , A , xhat_pre , xhat_post , P_pre , P_post , compute_P_smooth = False )
158156 x_hat_backward = xhat_smooth [:, 0 ][::- 1 ] # the result is backwards still, so reverse it
159157 dxdt_hat_backward = xhat_smooth [:, 1 ][::- 1 ]
160158
0 commit comments