-
-
Notifications
You must be signed in to change notification settings - Fork 7.1k
Fix & clean up EKF/UKF implementations #191
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
Pull Request Test Coverage Report for Build 843
💛 - Coveralls |
|
@regexident Hi. Thank you for your PR. Your code is very clean. I like it. |
|
@regexident However, I cannot understand some part of it. Please check the review. |
|
@atsushi-sakai sure! (I'm not seeing a review though, just two top-level comments of yours. |
AtsushiSakai
left a comment
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.
@regexident please check this.
|
|
||
| xd = motion_model(xd, ud) | ||
| xd = motion_model(xd, u) # assumes lack of noise | ||
| xTrue = motion_model(xTrue, u) + Qsim @ np.random.randn(4, 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.
Why you add noise to xTrue? xTrue is the true state without noise.
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 the system within which the "robot" operates is noisy, no?
You would never find a vehicle be able to move on a "perfectly" circular path in a real environment. There will always be tiny perturbations.
The dead-reckoning however works on the theoretical and thus clean inputs alone, and hence diverges from the true state over time.
It's the "clean x" state, that's wrong, not the noisy one (at least in regards to the process noise, it should be reasonably clean from observation noise).
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.
In my assumption, xTrue is the true robot state without noise, which is a circle trajectory. however dead-reckoning sensor has noise, so xd is a trajectory with dead-reckoning with noise.
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.
With real-life effects like wheel slippage, carpet "springiness" and uneven floors a robot is unlikely to ever drive a perfect circle, as the one seen in these demos.
Note that the "effect of an unknown input" from this …
(note that there is no
term since we have no known control inputs. Instead, we assume that
is the effect of an unknown input and
applies that effect to the state vector) …
… also applies to scenarios with a known control input, referred to as process noise (i.e. Qsim).
So as long as Qsim is anything but a zero-filled matrix you will get a true state that's not following a perfect path (as you'd get by dead-reckoning its pure inputs).
Additionally the current version of the code feeds the EKF/UKF with (noisy) x, y coordinate measurements, while feeding the DR with (noise-added) yawrate, velocity inputs, which is somewhat akin to comparing apple and oranges. If you wanted a "perfect circle" for xTrue you would need to keep Qsim at (near-)zero and probably change the measurements for both to be yawrate, velocity by changing these:
def observation_model(x):
# Observation Model
H = np.array([
[0, 0, 1, 0], // yaw rate
[0, 0, 0, 1] // velocity
])
z = H @ x
return z
def observation(x, u):
xTrue = motion_model(x, u) + Qsim @ np.random.randn(4, 1)
z = observation_model(x) + Rsim @ np.random.randn(2, 1)
uDR = u + Rsim @ np.random.randn(2, 1)
xDR = motion_model(xDR, uDR)
return xTrue, xDR, zAt which point you would get a perfect circle for xTrue and a noticeable drift for xDR.
The problem I would see with this however is that it would somewhat violate your goal 1.):
- Easy to read for understanding each algorithm's basic idea.
… by obfuscating the relationships between x, u, z, Q and R in Kalman Filters. As you're effectively making use of the fact that you input u is of the same structure as your output z, hence allowing you to perform u + Rsim @ np.random.randn(2, 1), which in the general case you cannot (due to likely differences in dimensionality and structure).
In contrast: the code in this PR expresses those relationships clearly and has them match the formulas found in text-books.
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.
With real-life effects like wheel slippage, carpet "springiness" and uneven floors a robot is unlikely to ever drive a perfect circle, as the one seen in these demos.
I know that but I think it is not needed, because the purpose of this code is to understand the EKF algorithm, not the behavior of a robot in a real situation..
| u = calc_input() | ||
|
|
||
| xTrue, z, xDR, ud = observation(xTrue, xDR, u) | ||
| xTrue, xDR, z = observation(xTrue, xDR, 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.
Why did you remove ud? ud is the input with noise.
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.
Because previously Qsim/Rsim were wrongly applied to u, instead of x/z, resulting in ud.
This observation is also backed by the example given on Wikipedia as well as several other articles on KFs.
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.
hmm... I think it is impossible to use u in a real situation, because every sensor for input (like wheel encoder and IMU) has noise.
AtsushiSakai
left a comment
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.
As I said, I like your clean code. But I cannot understand your point about the change related to the algorithm..
|
|
||
| xd = motion_model(xd, ud) | ||
| xd = motion_model(xd, u) # assumes lack of noise | ||
| xTrue = motion_model(xTrue, u) + Qsim @ np.random.randn(4, 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.
With real-life effects like wheel slippage, carpet "springiness" and uneven floors a robot is unlikely to ever drive a perfect circle, as the one seen in these demos.
I know that but I think it is not needed, because the purpose of this code is to understand the EKF algorithm, not the behavior of a robot in a real situation..
I did not so much change the algorithm, but rather correct it. The current version of This is supported by the fact, that the current implementations of EKF and UKF mix up # extended_kalman_filter.py
# Estimation parameter of EKF
Q = np.diag([0.1, 0.1, np.deg2rad(1.0), 1.0])**2 # ✅
R = np.diag([1.0, 1.0])**2 # ✅
# Simulation parameter
# ❌ this line seems incorrect: `z` is of structure `x, y`. No angles, so why the `deg2rad`?
Qsim = np.diag([1.0, np.deg2rad(30.0)])**2
Rsim = np.diag([0.5, 0.5])**2 # ✅
def observation(xTrue, xd, u):
# ❌ this is missing `… + Qsim @ np.random.randn(4, 1)` (see Wikipedia):
xTrue = motion_model(xTrue, u)
# add noise to gps x-y
# ❌ this seems incorrect:
# 1. `observation_model(xTrue)` should be used here to obtain `z`. Even if just to be explicit and clear.
# 2. this is missing `… + Rsim @ np.random.randn(2, 1)` (see Wikipedia)
# 3. `Rsim` is a `4 × 4` matrix, which is incompatible to `z`, a `2 × 1` vector
# 4. instead of doing element-wise algebra this is clearer expressed matrix-vector operations
zx = xTrue[0, 0] + np.random.randn() * Rsim[0, 0]
# ❌ same as line above
zy = xTrue[1, 0] + np.random.randn() * Rsim[1, 1]
# ❌ the `.T` transformation here is misleading and avoidable if expressed with matrix-vector operations.
z = np.array([[zx, zy]]).T
# add noise to input
# ❌ this seems incorrect:
# 1. `u` should not receive any noise. `x` should, instead (see Wikipedia)
# 2. `Qsim` is a `2 × 2` matrix, and `z`, a `2 × 1` vector, so by coincidence they are compatible.
# 3. In the general case `Qsim` and `z` are incompatible though, which may confuse an uneducated reader.
# 4. the first value of `u` is an angle, but in `Qsim` only the second element corresponds to an angle.
# 5. instead of doing element-wise algebra this is clearer expressed matrix-vector operations
ud1 = u[0, 0] + np.random.randn() * Qsim[0, 0]
# ❌ same as line above
ud2 = u[1, 0] + np.random.randn() * Qsim[1, 1]
# ❌ the `.T` transformation here is misleading and avoidable if expressed with matrix-vector operations.
ud = np.array([[ud1, ud2]]).T
xd = motion_model(xd, ud)
return xTrue, z, xd, ud# unscented_kalman_filter.py
# Estimation parameter of UKF
Q = np.diag([0.1, 0.1, np.deg2rad(1.0), 1.0])**2 # ✅
# ❌ this line seems incorrect: `z` is of structure `x, y`. No angles, so why the `deg2rad`?
R = np.diag([1.0, np.deg2rad(40.0)])**2
# Simulation parameter
# ❌ this line seems incorrect: `x` is a `4 × 1` vector, as such `Q` should be a `4 × 4` matrix
Qsim = np.diag([0.5, 0.5])**2
# ❌ this line seems incorrect: `z` is of structure `x, y`. No angles, so why the `deg2rad`?
Rsim = np.diag([1.0, np.deg2rad(30.0)])**2 # ⚠️
def observation(xTrue, xd, u):
# ❌ same issues as commented for EKF version
xTrue = motion_model(xTrue, u)
# add noise to gps x-y
# ❌ same issues as commented for EKF version, plus it's mixing up `Qsim` and `Rsim`
zx = xTrue[0, 0] + np.random.randn() * Qsim[0, 0]
# ❌ same as line above
zy = xTrue[1, 0] + np.random.randn() * Qsim[1, 1]
# ❌ same issues as commented for EKF version
z = np.array([[zx, zy]]).T
# add noise to input
# ❌ same issues as commented for EKF version, plus it's mixing up `Qsim` and `Rsim`
ud1 = u[0] + np.random.randn() * Rsim[0, 0]
# ❌ same as line above
ud2 = u[1] + np.random.randn() * Rsim[1, 1]
ud = np.array([ud1, ud2])
xd = motion_model(xd, ud)
return xTrue, z, xd, udTry modelling the original "version" of the algorithm with pure matrix-vector operations and you will see that they are not compatible dimension-wise, which supports my claim that the current algorithm is incorrect. |
|
If you want to have a "nice and clean" true path Qsim = np.diag([
0.00001, # variance of location on x-axis
0.00001, # variance of location on y-axis
0.00001, # variance of yaw-rate
0.00001, # variance of velocity
])**2
def jacobH(x):
jH = <calculate jacobian for new observation model>
return jH
def observation_model(x):
H = np.array([
[0, 0, 1, 0], # yaw-rate
[0, 0, 0, 1], # velocity
])
z = H @ x
return z
# replacement for `def observation(xTrue, xDR, u):`
def model(x, u):
x = motion_model(x, u) + Qsim @ np.random.randn(4, 1)
z = observation_model(x) + Rsim @ np.random.randn(2, 1)
return x, z
def main():
# ...
while SIM_TIME >= time:
# ...
xTrue, z = model(xTrue, u)
xEst, PEst = ekf_estimation(xEst, PEst, z, u)
# This is abusing the fact that `u` and `z` are of equal structure,
# thus allowing one to pass a measurement (i.e. `z`) as input (i.e. `u`).
# While this works in this particular instance of a KF, it will most likely lead to confusion.
xDR = motion_model(xDR, z)
# ...But again, I'd argue that this wouldn't really be beneficial to giving a clear picture of how Kalman Filters work, generalizably. |
|
@regexident OK. Thank you for your instructions. I will fix it up by myself later. I appreciate your interest : ). |
|
👍🏻 🙇🏻♂️ |
Issues addressed in this PR:
In Kalman Filters
Qis a square matrix with a sizeN × N, withNequal to the dimensions of the state vector.Ris a square matrix with a sizeM × M, withMequal to the dimensions of the output vector.extended_kalman_filter.py
Covariance matrices
In the EKF implementation …
4dimensions2dimensionsAs such
Qsimwould be expected to be of size4 × 4, yet was found to be of size2 × 2.Noise
Further more the noise was not applied correctly:
Qis not to be applied tou, but tox, as it is the state (i.e.x) noise covarianceRis not to be applied tox, but toz, as it is the state (i.e.z) noise covarianceunscented_kalman_filter.py
Covariance matrices
In the UKF implementation …
4dimensions2dimensionsAs such
Qsimwould be expected to be of size4 × 4, yet was found to be of size2 × 2.Further more
QsimandRsimseem to have been mixed up: The angle is a relevant part of the state not the output.Noise
Further more the noise was not applied correctly:
Ris not to be applied tou, but toz, as it is the state (i.e.z) noise covariancedef observation(xTrue, xd, u):
In both cases the implementation of
def observation(…):seems invalid to me.It should be:
See the formulas from Example application, technical on Wikipedia, specifically:
Where …
… implements …
… while …
… implements …
… while …
… assumes lack of noise, resulting in massive drifts for dead-reckoning.
The reason this code was incorrect, yet produced convincing results is that the dead-reckoning was actually fed noisy inputs, while EKF/UKF worked with mostly clean measurements.