Fix yaw gradient bug - #105
Open
vegard-solum-4ss wants to merge 1 commit into
Open
vegard-solum-4ss wants to merge 1 commit into
vegard-solum-4ss wants to merge 1 commit into
Conversation
'_yaw_gradient' returned the gradient of the yaw angle wrt. the global scaled Gibbs parameterization, a = 2 * q_v / q_w. The filters use it as the heading row of the measurement matrix, H, where the gradient must be taken wrt. the local body-frame attitude error, i.e. the error state dx[6:9] that is applied multiplicatively by '_update_quaternion_with_gibbs2'. The two agree only at roll = pitch = 0 (up to a cos(yaw / 2)**2 factor), which is why this went unnoticed. Away from level, the old gradient has a spurious x-component; the correct gradient has none, since the yaw angle is atan2(R_nb[1, 0], R_nb[0, 0]), a function of the body x-axis alone. The correct gradient is the bottom row of the Euler angle rate transformation matrix, [0, sin(roll) / cos(pitch), cos(roll) / cos(pitch)]. Affects the heading aiding of PVAMEKF, VAMEKF and AMEKF. On the beat benchmark with heading aiding, forward-filter attitude RMSE improves from [0.0089, 0.0118, 0.0013] to [0.0073, 0.0027, 0.0013] rad (roll, pitch, yaw), i.e. a 4.3x improvement in pitch. 'test__dhda' hardcoded the old values and is replaced by two tests: one against the closed form above, and one against a finite difference of '_yaw_from_quaternion' under '_update_quaternion_with_gibbs2', which pins the parameterization that was wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix: yaw gradient wrt. the attitude error state
Summary
_yaw_gradientreturned the gradient of the yaw angle wrt. the global scaledGibbs parameterization,
a = 2 * q_v / q_w. The filters use it as the heading row ofthe measurement matrix
H, where the gradient must be taken wrt. the localbody-frame attitude error — the error state
dx[6:9]that_resetappliesmultiplicatively via
_update_quaternion_with_gibbs2.The heading innovation itself was always correct, so heading stayed observable and the
filters converged. What was wrong is how that innovation is distributed into the other
states, which mainly cost roll and pitch accuracy.
Affects heading aiding in
PVAMEKF,VAMEKFandAMEKF.The bug
The two parameterizations coincide only at the identity attitude. At
roll = pitch = 0they differ by a scalar factor
cos(yaw / 2)**2— so at level attitude andyaw = 90°the old gradient was exactly half the correct value. Away from level they disagree in
direction as well:
The old gradient also has a spurious x-component. The correct gradient has none: yaw is
atan2(R_nb[1, 0], R_nb[0, 0]), a function of the body x-axis alone, and a rotationabout that same axis does not move it.
The fix
The gradient wrt. the local body-frame error is the bottom row of the Euler angle rate
transformation matrix:
Implemented in terms of the quaternion, to avoid computing Euler angles:
Conditioning is unchanged: in the old expression the
1 / u_x**2factor cancelledagainst
1 / (1 + u**2), leaving the samecos(pitch)**2denominator. Both aresingular only at
cos(pitch) = 0, where the yaw angle itself is undefined.Impact
Forward-filter attitude RMSE [rad] on
benchmark_full_pva_beat_202311Awith heading andgravity-reference aiding, 10 Hz, 30 min, 600 s warmup discarded:
A 4.3x improvement in pitch and 1.2x in roll. Yaw is unchanged, as expected — it was
directly observable either way.
Tests
test__dhdahardcoded the old values, so it is replaced by two tests:test__yaw_gradient— six attitudes checked against the closed form above.test__yaw_gradient_vs_finite_difference— five random quaternions checked against acentral difference of
_yaw_from_quaternionunder_update_quaternion_with_gibbs2.This one pins the parameterization that was wrong, so the bug cannot return silently.
Full suite passes (263 tests).
blackandisortclean.Notes
estimates than the forward filter. This fix is not the cause of that; the two are
independent and that work is on its own branch.
# type: ignore[no-any-return]is no longer needed, as the function now returns aconcrete
np.array.