Skip to content

feat(tools): obs_inspect R-IEKF vs standard yaw-leak comparison#426

Merged
Ravenwater merged 1 commit into
mainfrom
diag/obs-inspect-riekf-comparison
Jun 22, 2026
Merged

feat(tools): obs_inspect R-IEKF vs standard yaw-leak comparison#426
Ravenwater merged 1 commit into
mainfrom
diag/obs-inspect-riekf-comparison

Conversation

@Ravenwater

@Ravenwater Ravenwater commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends obs_inspect to measure the right-invariant (R-IEKF) yaw leak alongside the standard one on the same real clone windows — the real-data validation that R-IEKF fixes the #212 yaw-gauge over-confidence. Driven by the shipped build_invariant_measurement (no load-bearing change).

The measurement

For each MSCKF update, after perturbing the clone window by the filter's own per-clone σ (from P), it computes the yaw leak ‖H·N‖ under both parameterizations on identical geometry.

V2_03_difficult (2,918 updates):

parameterization mean yaw leak max yaw leak
standard MSCKF 0.389 619
R-IEKF (same perturbations) 2.3e-16 3.0e-14

The invariant gauge directions are estimate-independent constants (ρ = e_k, φ = ĝ), so the stacked invariant H annihilates the gauge regardless of the clone-window drift that makes the standard filter fabricate yaw information. R-IEKF eliminates the leak (~10¹⁵×) — the real-data complement to the synthetic invariant_vio_backend end-to-end test (yaw NEES 993 → <15).

Changes

  • tools/include/branes/tools/observability_inspect.hppobs_build_H_invariant (drives shipped build_invariant_measurement) + obs_build_N_invariant (constant invariant gauge).
  • tools/src/obs_inspect.cpp — per-update R-IEKF leak + summary verdict.
  • tests/tools/observability_inspect.cpp — gate: invariant leak stays ~0 under the same perturbation that leaks the standard one.
  • tools/README.md — updated.

Test Results

Target gcc clang
observability_inspect PASS (10 assertions / 3 cases) PASS (10 / 3)

Test plan

  • Fast CI passes (gcc + clang + MSVC + lint)
  • Promote to ready when satisfied

Relates to #212, #337, #347, #348

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added R-IEKF observability diagnostics to measure and compare yaw leaks across different filter parameterizations
  • Tests

    • Added test case verifying R-IEKF maintains observability under clone-window perturbations
  • Documentation

    • Updated observability diagnostic documentation to distinguish leak types and include R-IEKF measurements

Extends the observability diagnostic to measure the right-invariant (R-IEKF) leak
alongside the standard one on the SAME real clone windows, driving the shipped
build_invariant_measurement (invariant_update.hpp). For each perturbed window
(perturbed by the filter's own per-clone sigma) it now reports both the standard
yaw leak and the invariant yaw leak.

On V2_03_difficult the standard yaw leak is mean 0.39 / max 619 at the filter's own
claimed clone uncertainty, while the R-IEKF leak on the SAME perturbations is mean
2.3e-16 / max 3.0e-14 — eliminated. The invariant gauge directions are
estimate-independent constants (rho = e_k, phi = g), so the stacked invariant H
annihilates the gauge regardless of the clone-window drift that makes the standard
filter fabricate yaw information. This is the real-data evidence (complementing the
synthetic invariant_vio_backend end-to-end test, yaw NEES 993 -> <15) that the
right-invariant parameterization is the fix for the #212 yaw-gauge over-confidence.

Adds obs_build_H_invariant / obs_build_N_invariant to the header and a gate test
case asserting the invariant leak stays ~0 under the same perturbation that leaks
the standard one.

Relates to #212, #337, #347, #348

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds two header-only templated helpers (obs_build_H_invariant, obs_build_N_invariant) for R-IEKF observability diagnostics, extends the obs_inspect tool to compute and report invariant yaw leakage per update (JSONL and console), adds a Catch2 test asserting near-zero invariant yaw leak under SE(3) clone perturbations, and updates the README accordingly.

Changes

R-IEKF Invariant Observability Leak Diagnostics

Layer / File(s) Summary
obs_build_H_invariant and obs_build_N_invariant header helpers
tools/include/branes/tools/observability_inspect.hpp
Adds the invariant_update.hpp include and two new templated helpers: obs_build_H_invariant drives build_invariant_measurement to produce a stacked H_x/H_f Jacobian (zero-filled on degenerate M.ok); obs_build_N_invariant builds the estimate-independent 4-DoF gauge matrix with clone translation/yaw and hat(g)*p_f feature columns.
Invariant leak accumulation, JSONL, and console output
tools/src/obs_inspect.cpp
Adds sum_inv_yaw/max_inv_yaw accumulators; constructs Ninv per update; computes dyaw_inv inside the seeded perturbation loop via obs_build_H_invariant; emits leak_yaw_invariant in JSONL records; adds the R-IEKF averaged and max leak lines to the final console summary; updates the top-of-file description.
Catch2 test and README
tests/tools/observability_inspect.cpp, tools/README.md
New Catch2 test asserts near-zero translation/yaw leak at a consistent point and under random SE(3) clone-window perturbations (tolerance 1e-9). README observability section clarifies consistent/real leak distinction and documents the R-IEKF near-zero invariant yaw leak result.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • branes-ai/cortex#349: The new obs_build_H_invariant/obs_build_N_invariant helpers directly parallel the build_H_invariant/build_N_invariant construction introduced in that PR's right-invariant Jacobian/null-space work.
  • branes-ai/cortex#356: obs_build_H_invariant calls build_invariant_measurement from invariant_update.hpp, which that PR implements — direct dependency on the same invariant camera measurement update machinery.
  • branes-ai/cortex#396: Both PRs operate on the build_invariant_measurement Jacobian pipeline; that PR extends it to 6-DoF camera↔IMU extrinsics, which affects the H_x layout this PR maps into the stacked diagnostic Jacobian.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main change: extending obs_inspect to compare R-IEKF versus standard yaw-leak measurements, which aligns with the PR's core contribution.
Description check ✅ Passed The description includes a clear summary, itemized file changes, test results table, checklist items, and linked issues. All required sections are present and substantive.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch diag/obs-inspect-riekf-comparison

Comment @coderabbitai help to get the list of available commands and usage tips.

@Ravenwater Ravenwater self-assigned this Jun 22, 2026
@Ravenwater Ravenwater added this to the Phase 3: VIO milestone Jun 22, 2026
@Ravenwater Ravenwater marked this pull request as ready for review June 22, 2026 01:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/tools/observability_inspect.cpp`:
- Around line 115-118: The comment block above the std::mt19937_64 rng
initialization contains a wording error in the last line that says "yaw
observable by construction" which contradicts the gauge invariance being tested.
The test verifies that yaw remains UNOBSERVABLE (in the invariant null-space),
not observable, which is why the leak must stay ~0. Change the phrase from "yaw
observable by construction" to correctly state that yaw is UNOBSERVABLE by
construction to accurately reflect the gauge preservation property being
validated.
- Around line 108-113: The test does not validate that the invariant Jacobian
H_invariant is non-degenerate before checking the leak results. If
obs_build_H_invariant returns all zeros due to hitting a degenerate path, the
subsequent obs_leak assertions pass vacuously without actually validating the
invariant property. Add a non-degeneracy assertion on the result of
obs_build_H_invariant before the obs_leak checks at both locations (around line
111 and line 128). The assertion should verify that H_invariant contains
meaningful non-zero values rather than being all zeros, ensuring the invariant
property is genuinely validated.

In `@tools/README.md`:
- Around line 358-368: The README documentation overstates the invariant outputs
being measured and uses reversed observability terminology. At the section
discussing invariant leak measurements, remove the reference to
"invariant...translation leaks" since only invariant yaw accumulation is
actually reported by the implementation, not translation. Additionally, at the
end of this section where it states the R-IEKF "is observable-by-construction,"
this wording inverts the gauge claim being made—revise it to correctly describe
that the invariant gauge directions are estimate-independent constants (as
mentioned earlier in the same passage) rather than claiming
observability-by-construction.

In `@tools/src/obs_inspect.cpp`:
- Around line 271-276: The code at the invariant accumulation loop in
obs_inspect.cpp is currently accumulating only inv.second (yaw) via dyaw_inv
while discarding inv.first (translation) from the bt::obs_leak result. Clarify
the intent by either adding an explanatory comment above the dyaw_inv
accumulation line documenting why the translation component is intentionally
excluded (referencing that both parameterizations share the same translation
gauge), or if translation diagnostics should be tracked, add a corresponding
dtr_inv variable that accumulates inv.first similar to how dyaw_inv accumulates
inv.second, then divide dtr_inv by args.draws after the loop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a066cf4f-0f8a-489c-8804-1a034761af5d

📥 Commits

Reviewing files that changed from the base of the PR and between 8c54ed5 and 5beebfe.

📒 Files selected for processing (4)
  • tests/tools/observability_inspect.cpp
  • tools/README.md
  • tools/include/branes/tools/observability_inspect.hpp
  • tools/src/obs_inspect.cpp

Comment on lines +108 to +113
const auto Ninv = bt::obs_build_N_invariant<T>(truth, p_f, g);

// Consistent point: the shipped invariant Jacobian annihilates the invariant gauge.
const auto [tr0, yaw0] = bt::obs_leak<T>(bt::obs_build_H_invariant<T>(truth, p_f, ext), Ninv);
REQUIRE_THAT(tr0, WithinAbs(0.0, 1e-9));
REQUIRE_THAT(yaw0, WithinAbs(0.0, 1e-9));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prevent a vacuous pass when invariant Jacobian construction degenerates.

At Lines 111 and 128, the test only checks obs_leak(...) == ~0. If obs_build_H_invariant hits its degenerate path and returns zeros, these assertions pass without validating the invariant property. Add a non-degeneracy assertion on H_invariant before leak checks.

Proposed patch
-    const auto [tr0, yaw0] = bt::obs_leak<T>(bt::obs_build_H_invariant<T>(truth, p_f, ext), Ninv);
+    const auto H0 = bt::obs_build_H_invariant<T>(truth, p_f, ext);
+    REQUIRE(H0.rows() > 0);
+    REQUIRE(H0.cwiseAbs().maxCoeff() > 1e-12);
+    const auto [tr0, yaw0] = bt::obs_leak<T>(H0, Ninv);
@@
-    const auto [tr_p, yaw_p] = bt::obs_leak<T>(bt::obs_build_H_invariant<T>(e, p_f, ext), Ninv);
+    const auto Hp = bt::obs_build_H_invariant<T>(e, p_f, ext);
+    REQUIRE(Hp.rows() > 0);
+    REQUIRE(Hp.cwiseAbs().maxCoeff() > 1e-12);
+    const auto [tr_p, yaw_p] = bt::obs_leak<T>(Hp, Ninv);

Also applies to: 128-130

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/tools/observability_inspect.cpp` around lines 108 - 113, The test does
not validate that the invariant Jacobian H_invariant is non-degenerate before
checking the leak results. If obs_build_H_invariant returns all zeros due to
hitting a degenerate path, the subsequent obs_leak assertions pass vacuously
without actually validating the invariant property. Add a non-degeneracy
assertion on the result of obs_build_H_invariant before the obs_leak checks at
both locations (around line 111 and line 128). The assertion should verify that
H_invariant contains meaningful non-zero values rather than being all zeros,
ensuring the invariant property is genuinely validated.

Comment on lines +115 to +118
// Same clone-window perturbation that makes the STANDARD yaw leak (previous
// test). The invariant gauge directions are constants (rho = e_k, phi = g), so
// the leak must STAY ~0 — yaw observable by construction. This is the fix.
std::mt19937_64 rng(0x0B5E11ull);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Line 117 wording inverts the gauge claim.

“yaw observable by construction” conflicts with the invariant null-space argument being tested here; this path is about preserving the yaw unobservable gauge (leak stays ~0). Please reword to avoid future confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/tools/observability_inspect.cpp` around lines 115 - 118, The comment
block above the std::mt19937_64 rng initialization contains a wording error in
the last line that says "yaw observable by construction" which contradicts the
gauge invariance being tested. The test verifies that yaw remains UNOBSERVABLE
(in the invariant null-space), not observable, which is why the leak must stay
~0. Change the phrase from "yaw observable by construction" to correctly state
that yaw is UNOBSERVABLE by construction to accurately reflect the gauge
preservation property being validated.

Comment thread tools/README.md
Comment on lines +358 to +368
consistent/real/invariant yaw & translation leaks. On V2_03 the consistent leak is
~1e-15 (the shipped Jacobian is correct), the translation leak is **structurally 0**
(the ±Hf cancellation protects it), and the **standard yaw leak is nonzero on 100%
of updates and grows with the clone window's attitude drift** — localizing the #212
over-confidence to the yaw gauge on real data.

It also measures the **right-invariant (R-IEKF) leak on the same perturbed clone
windows** (driving the shipped `build_invariant_measurement`): because the
invariant gauge directions are estimate-independent constants (ρ = e_k, φ = ĝ), the
invariant yaw leak stays ~0 where the standard one rises — the real-data evidence
that R-IEKF is observable-by-construction and is the fix the diagnosis points to.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

README overstates invariant outputs and uses inverted observability wording.

At Line 358, docs currently imply invariant yaw and translation leaks are reported, but the implementation context shows invariant yaw accumulation/reporting only. At Line 368, “observable-by-construction” reverses the gauge claim in this section.

Proposed patch
-consistent/real/invariant yaw & translation leaks. On V2_03 the consistent leak is
+consistent/real yaw & translation leaks, plus invariant yaw leak. On V2_03 the consistent leak is
@@
-invariant yaw leak stays ~0 where the standard one rises — the real-data evidence
-that R-IEKF is observable-by-construction and is the fix the diagnosis points to.
+invariant yaw leak stays ~0 where the standard one rises — the real-data evidence
+that R-IEKF preserves the unobservable gauge by construction and is the fix the diagnosis points to.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/README.md` around lines 358 - 368, The README documentation overstates
the invariant outputs being measured and uses reversed observability
terminology. At the section discussing invariant leak measurements, remove the
reference to "invariant...translation leaks" since only invariant yaw
accumulation is actually reported by the implementation, not translation.
Additionally, at the end of this section where it states the R-IEKF "is
observable-by-construction," this wording inverts the gauge claim being
made—revise it to correctly describe that the invariant gauge directions are
estimate-independent constants (as mentioned earlier in the same passage) rather
than claiming observability-by-construction.

Comment thread tools/src/obs_inspect.cpp
Comment on lines +271 to +276
const auto inv = bt::obs_leak<T>(bt::obs_build_H_invariant<T>(pe, p_f, cal.extrinsics), Ninv);
dyaw_inv += static_cast<double>(inv.second);
}
dyaw /= args.draws;
dtr /= args.draws;
dyaw_inv /= args.draws;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Invariant translation leak discarded — intentional?

Line 272 accumulates only inv.second (yaw); inv.first (translation) is dropped. Both parameterizations share the same translation gauge, so this is likely intentional for the yaw-dominance focus. If future diagnostics need translation comparisons, consider storing it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/src/obs_inspect.cpp` around lines 271 - 276, The code at the invariant
accumulation loop in obs_inspect.cpp is currently accumulating only inv.second
(yaw) via dyaw_inv while discarding inv.first (translation) from the
bt::obs_leak result. Clarify the intent by either adding an explanatory comment
above the dyaw_inv accumulation line documenting why the translation component
is intentionally excluded (referencing that both parameterizations share the
same translation gauge), or if translation diagnostics should be tracked, add a
corresponding dtr_inv variable that accumulates inv.first similar to how
dyaw_inv accumulates inv.second, then divide dtr_inv by args.draws after the
loop.

@Ravenwater Ravenwater merged commit f05a5e7 into main Jun 22, 2026
15 checks passed
@Ravenwater Ravenwater deleted the diag/obs-inspect-riekf-comparison branch June 22, 2026 01:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant