Skip to content

S9 — Marginalization

The sliding window has to stay bounded, so each step S9 drops the oldest clone (and any features anchored only to it). For a filter, this is exact principal-submatrix extraction of the kept states — and its correctness check is a clean mathematical identity.

Slide the window: remove a clone’s 6 error-state rows/columns from the covariance. In covariance form this is principal-submatrix extraction of the kept states (a principal submatrix of a PD matrix stays PD). The clone is selected by policy (oldest, or a two-way keyframe / non-keyframe scheme).

The defining property of marginalization: marginalizing a variable never alters the marginal of the others. So after dropping a clone, every kept state’s covariance — and the cross-covariances among kept states — must be exactly unchanged. If they shift, the extraction is wrong (or the covariance wasn’t consistent going in). For a filter doing submatrix extraction there is no FEJ / marginalization-prior subtlety (unlike a smoother); correctness rides entirely on the S3 augmentation cross-covariance having been right.

Signaturemarginalize(x, P, clone_idx) → (x', P') (slide the window)
Preclone selected by policy (oldest, or two-way keyframe/non-keyframe)
Post / invariantsP' symmetric PSD · kept-state marginal unchanged by removing a clone (pure extraction) · marginalization does not constrain the 4 gauge directions
cortexmsckf/state_helper.hppmarginalize_clone, principal submatrix via cov.marginalize(keep)

The S9 probe builds a three-clone correlated covariance, drops the oldest clone via the shipped marginalize_clone, and checks the kept-marginal identity directly:

invariantresidualverdict
kept-state marginal unchanged0PASS — pure extraction
P' positive semidefinitePASS
state dimension33 → 27shrinks by one 6-DoF clone
window length3 → 2 clonesstays bounded

The kept-marginal residual is identically zerocov.marginalize(keep) is an exact principal-submatrix extraction, so dropping the oldest clone leaves every retained state’s covariance untouched, and P' stays PSD.

The measurements above come from the synthetic probe. Its real-data companion, s9_inspect (the inverse of s3_inspect), runs the real estimator over a real EuRoC sequence until the window holds several clones, then takes a copy of that keyframe state and runs the real StateHelper::marginalize_clone on it. No SDK change — the operator is standalone and backend().state() is exposed.

Terminal window
s9_inspect --dataset /path/to/V1_01_easy/mav0 --out build/s9 --min-clones 5 --idx 0
node docs-site/scripts/gen-marginalization-figures.mjs build/s9 # → marginalize_covariance.svg

It writes marginalization.json and renders the before/after covariance heatmap with the dropped clone block outlined in the “before” matrix — its row band and column band (the cross-covariance) are what’s discarded. The HUD reports the invariant on real data: the kept-state marginal residual (the others’ covariance must be untouched) and the information dropped (the clone’s own σ and its cross-covariance with the kept states). On EuRoC the kept-marginal residual comes out identically zero, confirming the exact extraction holds on a real, fully-correlated filter covariance, not just the synthetic one.