Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions tools/include/branes/tools/s6_update_inspect.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ struct S6UpdateRecord {
double cov_trace_before = 0.0; ///< tr(P) before the update
double cov_trace_after = 0.0; ///< tr(P) after (== before if rejected)
double cov_trace_ratio = 1.0; ///< after / before (<1 ⇒ uncertainty shrank)
bool psd_after = true; ///< P stayed positive-definite (Joseph guarantee)
bool psd_after = true; ///< P stayed positive-SEMI-definite (Joseph guarantee; the
///< MSCKF gauge + stochastic-clone copy make P legitimately rank-deficient)
double residual_rms_px = 0.0; ///< RMS reprojection residual at the landmark
std::vector<S6ObsResidual> residuals;
};
Expand Down Expand Up @@ -150,7 +151,13 @@ class S6UpdateInspector {
const DynMat cov_after = state_after.covariance();
r.cov_trace_after = trace(cov_after);
r.cov_trace_ratio = r.cov_trace_before > 0.0 ? r.cov_trace_after / r.cov_trace_before : 1.0;
r.psd_after = ms::is_positive_definite(cov_after);
// The MSCKF covariance is legitimately rank-deficient: the newest clone is a
// stochastic copy of the IMU pose (exact zero-covariance directions) and the
// 4-DoF gauge (yaw + global position) is unobservable. Those eigenvalues sit at
// 0 ± machine-eps, so a strict positive-DEFINITE (bare Cholesky) test reports
// false breakage on every partial-window update. Positive-SEMI-definite (small
// ridge) is the correct invariant the Joseph form actually guarantees.
r.psd_after = ms::is_positive_semidefinite(cov_after);

// Per-observation reprojection residual at the triangulated landmark.
branes::math::lie::detail::Vec<double, 3> p_f{};
Expand Down
20 changes: 17 additions & 3 deletions tools/src/s6_inspect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,10 @@ int main(int argc, char** argv) {

double frame_t = 0.0;
std::uint64_t n_updates = 0, n_accepted = 0, n_gated = 0, n_psd_fail = 0;
double sum_nis_over_dof = 0.0;
double sum_nis_over_dof = 0.0; // all valid updates (incl. χ²-gated outlier tail)
std::size_t nis_count = 0;
double sum_nis_accepted = 0.0; // accepted updates only — the honest per-update consistency
std::size_t nis_accepted_count = 0;
bool dumped = false;
json cov_dump;

Expand All @@ -281,6 +283,10 @@ int main(int argc, char** argv) {
if (rec.valid && rec.dof > 0) {
sum_nis_over_dof += rec.nis_over_dof;
++nis_count;
if (rec.accepted) {
sum_nis_accepted += rec.nis_over_dof;
++nis_accepted_count;
}
}

// Dump one update's full covariance for the before/after heatmap.
Expand Down Expand Up @@ -341,13 +347,21 @@ int main(int argc, char** argv) {
}
}

// Accepted-only NIS is the honest per-update consistency: the all-valid mean is
// dominated by the χ²-gated outlier tail (rejected, never applied), so it reads
// alarmingly high while the corrections the filter actually made are consistent.
// System-level over-confidence (#212) is a STATE-NEES-vs-ground-truth property and
// is measured end-to-end (vio_pipeline), not from these innovation residuals.
const double mean_nod = nis_count ? sum_nis_over_dof / static_cast<double>(nis_count) : 0.0;
const double mean_nod_acc = nis_accepted_count ? sum_nis_accepted / static_cast<double>(nis_accepted_count) : 0.0;
std::cout << "s6_inspect: processed " << processed << " frames";
if (skipped)
std::cout << " (" << skipped << " unreadable, skipped)";
std::cout << "\n updates: " << n_updates << " (" << n_accepted << " accepted, " << n_gated << " χ²-gated)\n"
<< " NIS/dof: mean " << mean_nod << " over " << nis_count << " valid updates"
<< (mean_nod > 1.2 ? " → over-confident (R under-modeled)" : "") << "\n";
<< " NIS/dof: accepted " << mean_nod_acc << " over " << nis_accepted_count << " applied updates"
<< (mean_nod_acc > 1.5 ? " → applied updates over-confident" : " → applied updates consistent")
<< "\n all-valid " << mean_nod << " over " << nis_count
<< " (incl. gated outlier tail — not the consistency metric)\n";
if (n_psd_fail)
std::cout << " WARNING: " << n_psd_fail << " updates left P non-PSD\n";
std::cout << " records: " << upd_path << "\n";
Expand Down
Loading