-
Notifications
You must be signed in to change notification settings - Fork 22
addressing #68, #69, #71, #95, and #96 for the kalman module #98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| - **x_hat** -- estimated (smoothed) x | ||
| - **dxdt_hat** -- estimated derivative of x | ||
| """ | ||
| w = np.arange(len(x)) / (len(x) - 1) # set up weights, [0., ... 1.0] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's an easier way.
| #################### | ||
|
|
||
|
|
||
| def __kalman_forward_update__(xhat_fm, P_fm, y, u, A, B, C, R, Q): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found making this a separate function to be unnecessary; its logic now lives in the forward filter.
| else: | ||
| xhat_fp = xhat_fm | ||
| P_fp = (I - K_f@C)@P_fm | ||
| xhat_fm = A@xhat_fp + B@u |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some duplicated lines in here. I got rid of them.
| :return: | ||
| """ | ||
| I = np.array(np.eye(A.shape[0])) | ||
| gammaW = np.array(np.eye(A.shape[0])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this supposed to do? It was doing nothing, so I got rid of it.
| - **xhat_pre** -- a priori estimates of xhat, with axis=0 the batch dimension, so xhat[n] gets the nth step | ||
| - **xhat_post** -- a posteriori estimates of xhat | ||
| - **P_pre** -- a priori estimates of P | ||
| - **P_post** -- a posteriori estimates of P |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've renamed all these to be a little more explicit about what they are.
| :param np.array u: optional control input | ||
| :param np.array B: optional control matrix | ||
| :return: tuple[np.array, np.array, np.array, np.array] of\n | ||
| - **xhat_pre** -- a priori estimates of xhat, with axis=0 the batch dimension, so xhat[n] gets the nth step |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I many places you weren't using 0 as the batch dimension. But axis 0 should always be the batch dimension so you can pull out a sample without having to do complicated slicing.
| xhat_fp = xhat_fm + K_f@(y - C@xhat_fm) | ||
| P_fp = (I - K_f@C)@P_fm | ||
| xhat_fm = A@xhat_fp + B@u | ||
| P_fm = A@P_fp@A.T + gammaW@Q@gammaW.T |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The order of these calls is inverted from how I tend to think about the KF. You can start on either the prediction or the update side, but I start on prediction in the paper, and so do the standard Wikipedia and many other pseudocodes for the KF. Thus, your _fm variables here contain the next time step's prediction, but they're indexed at this time step, which causes confusion when you go do your RTS implementation.
| xhat_smooth = copy.copy(xhat_fp) | ||
| P_smooth = copy.copy(P_fp) | ||
| for t in range(N-2, -1, -1): | ||
| L = P_fp[t]@A.T@np.linalg.pinv(P_fm[t]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this line is actually correct, because P_fm[t] holds
| for t in range(N-2, -1, -1): | ||
| L = P_fp[t]@A.T@np.linalg.pinv(P_fm[t]) | ||
| xhat_smooth[:, [t]] = xhat_fp[:, [t]] + L@(xhat_smooth[:, [t+1]] - xhat_fm[:, [t+1]]) | ||
| P_smooth[t] = P_fp[t] - L@(P_smooth[t+1] - P_fm[t+1]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this line is incorrect, because you're supposed to use P_fm[t]. https://arc.aiaa.org/doi/10.2514/3.3166
This line is also wrong because you're subtracting the L part instead of adding it. So this isn't doing RTS smoothing at all. Serious bug.
| ##################### | ||
|
|
||
|
|
||
| def __constant_velocity__(x, dt, params, options=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need all these shadow functions behind the front-facing ones, because you should really only define the matrices once and then call smoothing twice. As written, the matrices were getting redefined for the forward and backward passes separately, extra work. So the two levels got combined across all these methods.
| ######################################### | ||
| # Constant 1st, 2nd, and 3rd derivative # | ||
| ######################################### | ||
| def _constant_derivative(x, P0, A, C, R, Q, forwardbackward) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So so so so much code in this module was just copy and pasted, and the structure to call things either forward or forward and backward was repeated unnecessarily, so I've put that logic in a single place.
| x_hat_forward = xhat_smooth[:, 0] # first dimension is time, so slice first element at all times | ||
| dxdt_hat_forward = xhat_smooth[:, 1] | ||
|
|
||
| if not forwardbackward: # bound out here if not doing the same in reverse and then combining |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's very odd to me that the Kalman with RTS smoothing already works forward and backward, but then you invert the dynamics and go again and then take a weighting of the answers. Why? I thought RTS smoothing was already supposed to be optimal. Did it not seem to work at one point, and these were further hacks? I wonder if having the bugs in the RTS smoother fixed will make the hacks unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The answer is that a non-optimal guess to kick off the process biases the end you start on, and it takes a while for that bias to wear off. Classic RTS (even sans bugs) doesn't fully address this, but doing two of them and weighting together does.
Ideally, you could choose an initial x0,P0 that wouldn't horribly bias the process, but due to x0,P0. See my ponderings on #97.
| x_hat = x_hat_forward*w + x_hat_backward*(1-w) | ||
| dxdt_hat = dxdt_hat_forward*w + dxdt_hat_backward*(1-w) | ||
|
|
||
| dxdt_hat_corrected = np.mean((dxdt_hat, dxdt_hat_forward), axis=0) # What is this line for? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This strikes me as a hack. I don't like it.
| :type params: dict {'backward': boolean}, optional | ||
|
|
||
| :return: a tuple consisting of: | ||
| def constant_acceleration(x, dt, params=None, options=None, r=None, q=None, forwardbackward=True): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, these function signatures repeat a bunch of docstring and input-checking logic, but I can't simplify further without breaking backwards compatibility. I'd prefer a single constant_derivative function that takes order as a parameter.
| :return: matrix: | ||
| - xhat_smooth: smoothed estimates of the full state x | ||
| :rtype: tuple -> (np.array, np.array) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docstring indicates here and in its topline by use of the words "to estimate the derivative" that this function should be calculating a derivative, but it's not. This begs the question: Does anybody use this, since it went unnoticed? Can I break backwards compatibility for this function alone?
|
|
||
|
|
||
| ################################################################################################### | ||
| # Constant Acceleration with Savitzky-Golay pre-estimate (not worth the parameter tuning trouble) # |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is true, then we should nuke this code per #95. If we ever wish to recover the experiment, we can go back to an ancient tagged commit and fish it out of the nether.
| if len(x.shape) == 2: | ||
| pass | ||
| else: | ||
| x = np.reshape(x, [1, len(x)]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not necessary to reshape 1D vectors into vertical 2D column vectors, because A @ x will do the exact same thing in either case.
|
|
||
| xhat_fp = None | ||
| P_fp = [] | ||
| P_fm = [P_fm] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This initialization is slightly odd, because it says x0, P0 are the a priori estimate for the current time based on the previous time, rather than considering them the a posteriori estimate from last time. The difference is that last time's a posteriori estimate would be propagated forward before projection through
What makes more sense when we ask the user for an initial condition? "Tell us where you think the system is, one time step before seeing any measurements" or "Tell us where you think the system is at the moment of the first measurement"? Six on one hand, half dozen on the other. I spent a good while in #97 trying to think of a good way to initialize the cycle, but any projection from measurement space to full state space requires the measurement space to have the same dimension as the state space, which just isn't our case at all for all the constant derivative stuff, where the dimension of x goes up to the
In the end I decided I prefer to project our identity matrix P0 guess through
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we end up preferring the opposite, it's easy to change _kalman_forward_filter.
| :param Q: | ||
| :return: | ||
| if B is None: B = np.zeros((A.shape[0], 1)) | ||
| if u is None: u = np.zeros(B.shape[1]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
u can safely be 1D.
| elif r == None or q == None: | ||
| raise ValueError("`q` and `r` must be given.") | ||
|
|
||
| A = np.array([[1, dt, (dt**2)/2], # states are x, x', x" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm using a more exact A here, per #96
…cstrings-and-doublecheck
…into kalman-docstrings-and-doublecheck
|
This one now makes use of the changes from #102, so order of operations says that one should be reviewed and merged first. Then we can |





The tests are definitely going to fail, because I identified at least one bug that changes core logic, so the results will be a tiny bit numerically different. We'll check against improved unit tests once that's merged. This also addresses #97 some, but I'm afraid that one will take significantly more work if we want to actually properly extend this module. Even the linear KF in this module isn't calculating a derivative (mistake?), which makes me extra suspicious that it hasn't had any love in a long time.