Skip to content

Commit 9143fa7

Browse files
committed
think I sorted it out
1 parent 5f360a9 commit 9143fa7

8 files changed

Lines changed: 170 additions & 365 deletions

examples/1_basic_tutorial.ipynb

Lines changed: 28 additions & 172 deletions
Large diffs are not rendered by default.

examples/2a_optimizing_parameters_with_dxdt_known.ipynb

Lines changed: 26 additions & 52 deletions
Large diffs are not rendered by default.

examples/2b_optimizing_parameters_with_dxdt_unknown.ipynb

Lines changed: 26 additions & 52 deletions
Large diffs are not rendered by default.

examples/3_automatic_method_suggestion.ipynb

Lines changed: 13 additions & 10 deletions
Large diffs are not rendered by default.

pynumdiff/kalman_smooth/_kalman_smooth.py

Lines changed: 50 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -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

97105
def 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

pynumdiff/linear_model/_linear_model.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,11 +246,11 @@ def rbfdiff(x, _t, sigma=1, lmbd=0.01):
246246
- **x_hat** -- estimated (smoothed) x
247247
- **dxdt_hat** -- estimated derivative of x
248248
"""
249-
if isinstance(_t, (np.ndarray, list)): # support variable step size for this function
249+
if np.isscalar(_t):
250+
t = np.arange(len(x))*_t
251+
else: # support variable step size for this function
250252
if len(x) != len(_t): raise ValueError("If `_t` is given as array-like, must have same length as `x`.")
251253
t = _t
252-
else:
253-
t = np.arange(len(x))*_t
254254

255255
# The below does the approximate equivalent of this code, but sparsely in O(N sigma), since the rbf falls off rapidly
256256
# t_i, t_j = np.meshgrid(t,t)

pynumdiff/polynomial_fit/_polynomial_fit.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ def splinediff(x, _t, params=None, options={}, degree=3, s=None, num_iterations=
3030
if 'iterate' in options and options['iterate']:
3131
num_iterations = params[2]
3232

33-
if isinstance(_t, (np.ndarray, list)): # support variable step size for this function
33+
if np.isscalar(_t):
34+
t = np.arange(len(x))*_t
35+
else: # support variable step size for this function
3436
if len(x) != len(_t): raise ValueError("If `_t` is given as array-like, must have same length as `x`.")
3537
t = _t
36-
else:
37-
t = np.arange(len(x))*_t
3838

3939
x_hat = x
4040
for _ in range(num_iterations):

pynumdiff/tests/test_diff_methods.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ def spline_irreg_step(*args, **kwargs): return splinediff(*args, **kwargs)
4444
(spline_irreg_step, {'degree':5, 's':2}),
4545
(polydiff, {'degree':2, 'window_size':3}), (polydiff, [2, 3]),
4646
(savgoldiff, {'degree':2, 'window_size':5, 'smoothing_win':5}), (savgoldiff, [2, 5, 5]),
47-
(constant_velocity, {'r':1e-4, 'q':1e-2}), (constant_velocity, [1e-4, 1e-2]),
48-
(constant_acceleration, {'r':1e-4, 'q':1e-1}), (constant_acceleration, [1e-4, 1e-1]),
49-
(constant_jerk, {'r':1e-4, 'q':10}), (constant_jerk, [1e-4, 10]),
47+
(constant_velocity, {'r':1e-2, 'q':1e3}), (constant_velocity, [1e-2, 1e3]),
48+
(constant_acceleration, {'r':1e-3, 'q':1e4}), (constant_acceleration, [1e-3, 1e4]),
49+
(constant_jerk, {'r':1e-4, 'q':1e5}), (constant_jerk, [1e-4, 1e5]),
5050
(velocity, {'gamma':0.5}), (velocity, [0.5]),
5151
(acceleration, {'gamma':1}), (acceleration, [1]),
5252
(jerk, {'gamma':10}), (jerk, [10]),
@@ -184,24 +184,24 @@ def spline_irreg_step(*args, **kwargs): return splinediff(*args, **kwargs)
184184
[(-1, -1), (0, 0), (0, -1), (1, 0)],
185185
[(0, 0), (2, 2), (0, 0), (2, 2)],
186186
[(1, 1), (3, 3), (1, 1), (3, 3)]],
187-
constant_velocity: [[(-25, -25), (-25, -25), (0, -1), (0, 0)],
188-
[(-3, -3), (-2, -2), (0, -1), (0, 0)],
189-
[(-1, -2), (0, 0), (0, -1), (0, 0)],
190-
[(-1, -1), (1, 0), (0, -1), (1, 0)],
191-
[(1, 1), (2, 2), (1, 1), (2, 2)],
192-
[(1, 1), (3, 3), (1, 1), (3, 3)]],
193-
constant_acceleration: [[(-25, -25), (-25, -25), (0, -1), (0, 0)],
194-
[(-3, -4), (-3, -3), (0, -1), (0, 0)],
195-
[(-3, -3), (-2, -2), (0, -1), (0, 0)],
196-
[(-1, -1), (0, 0), (0, -1), (0, 0)],
197-
[(1, 1), (2, 2), (1, 1), (2, 2)],
198-
[(1, 1), (3, 3), (1, 1), (3, 3)]],
199-
constant_jerk: [[(-25, -25), (-25, -25), (0, -1), (0, 0)],
200-
[(-4, -5), (-3, -4), (0, -1), (0, 0)],
201-
[(-3, -4), (-2, -3), (0, -1), (0, 0)],
202-
[(-1, -2), (0, 0), (0, -1), (0, 0)],
203-
[(1, 0), (2, 1), (1, 0), (2, 1)],
204-
[(1, 1), (3, 3), (1, 1), (3, 3)]],
187+
constant_velocity: [[(-25, -25), (-25, -25), (0, -1), (1, 1)],
188+
[(-4, -5), (-3, -3), (0, -1), (1, 1)],
189+
[(-3, -3), (0, 0), (0, -1), (1, 1)],
190+
[(-3, -3), (1, 0), (0, -1), (1, 1)],
191+
[(-1, -1), (2, 2), (0, 0), (2, 2)],
192+
[(-1, -1), (3, 3), (0, 0), (3, 3)]],
193+
constant_acceleration: [[(-25, -25), (-25, -25), (0, -1), (1, 1)],
194+
[(-5, -5), (-4, -4), (0, -1), (1, 1)],
195+
[(-4, -5), (-3, -3), (0, -1), (1, 1)],
196+
[(-3, -3), (0, 0), (0, -1), (1, 1)],
197+
[(-1, -1), (1, 1), (0, -1), (1, 1)],
198+
[(0, 0), (3, 3), (0, 0), (3, 3)]],
199+
constant_jerk: [[(-25, -25), (-25, -25), (0, -1), (1, 1)],
200+
[(-6, -6), (-5, -5), (0, -1), (1, 1)],
201+
[(-5, -5), (-4, -4), (0, -1), (1, 1)],
202+
[(-3, -3), (-1, -1), (0, -1), (1, 1)],
203+
[(-1, -1), (1, 1), (0, -1), (1, 1)],
204+
[(0, 0), (3, 3), (0, 0), (3, 3)]],
205205
spectraldiff: [[(-9, -10), (-14, -15), (-1, -1), (0, 0)],
206206
[(0, 0), (1, 1), (0, 0), (1, 1)],
207207
[(1, 1), (1, 1), (1, 1), (1, 1)],

0 commit comments

Comments
 (0)