Skip to content

Invariants — the Oracle

A bench is only as good as its oracle. The invariant library (#445) lives in sdk/include/branes/sdk/eval/invariants/ (namespace branes::sdk::eval::inv). It has one check per invariant of every stage, so a wrong output is caught at the transformation that produced it.

Every check returns an InvariantResult:

FieldMeaning
name, stagea stable id such as covariance.psd, and the stage it belongs to (S9_marginalization)
valuethe measured residual or statistic, in native units (rad, m², px, s, count, dimensionless)
threshold, threshold_hithe bound it is held to, in the same units
boundUpper (a residual), Lower (a margin, e.g. λ_min of an SPD matrix), Band (e.g. NIS/dof), or Report (context only, never fails)
passthe verdict; a NaN or a non-finite input never passes

Results collect into an InvariantReport, which answers three questions:

  • first_failure(): in pipeline order, the first stage that broke something.
  • tightest(): which invariant is closest to failing.
  • for_stage(s): what one stage reported.

The report stores values in double, which holds every value of float, posit<32,2> and double exactly, so one report can mix stages that ran in different number systems.

A threshold that comes from floating-point arithmetic is never a hardcoded 1e-9. It is

τ  =  safety⋅n⋅εT⋅scale,\tau \;=\; \text{safety}\cdot n\cdot \varepsilon_T\cdot \text{scale},

where:

  • εT\varepsilon_T is the unit roundoff of the type under test;
  • nn is the size of the computation (the γn=n u\gamma_n = n\,u growth of the standard backward-error bounds);
  • scale is the norm the residual is measured against, which gives the threshold native units.

So the same check means the same thing in float, posit16, posit<32,2> and double. Where a residual is a backward error, for example the Kalman-gain solve ∥KS−PH⊤∥F/(∥K∥F∥S∥F+∥PH⊤∥F)\|KS - PH^\top\|_F / (\|K\|_F\|S\|_F + \|PH^\top\|_F), the conditioning of SS cancels out of the bound, and a stable solve passes in any type however ill-conditioned SS is.

Physical bounds stay explicit parameters of the check, never defaults dressed up as universal. These include Δt limits, pixel margins, and the static-window ‖a‖ ≈ g tolerance.

StageInvariantsChecks
Inputstimestamps strictly increasing; Δt within the sensor-rate bounds; samples finite and in range; static ‖a‖ ≈ g; R_imu_cam ∈ SO(3); intrinsics positive and inside the imagecheck_timestamps_increasing, check_dt_bounds, check_finite, check_bounded, check_static_gravity, check_so3, check_intrinsics
S0_sensor_modelproject/unproject round trip; analytic Jacobians vs finite differencescheck_round_trip, check_jacobian_fd
S1_initializationgravity sign and magnitude; biases bounded; P₀ SPD; attitude ∈ SO(3)check_gravity, check_bounded, check_spd, check_so3
S2_propagationR ∈ SO(3); Q_d ⪰ 0; P = ΦPΦᵀ + Q_d, symmetric, ⪰ 0; Φ preserves the 4-D unobservable subspacecheck_so3, check_psd, check_covariance_propagation, check_subspace_preserved
S3_augmentation+6 per clone; blocks equal J P Jᵀ and J P; P ⪰ 0check_augmentation, check_psd
S4_frontendtracks inside the image; KLT forward–backward bound; track length vs clone windowcheck_points_in_image, check_bounded, check_track_lengths
S5_triangulationpositive depth; reprojection bound; parallax and normal-matrix κ (reported)check_at_least, check_bounded, report_condition_number
S6a_jacobiansH_x, H_f vs finite differencescheck_jacobian_fd
S6b_nullspace_projection‖NᵀH_f‖ ≈ 0, NᵀN = I, rank 2m − 3check_annihilates, check_orthonormal_rows, check_rank
S6c_compressionQR compression preserves HᵀH and Hᵀr; ‖r_c‖ ≤ ‖r‖check_compression
S6d_gatingS SPD; NIS ~ χ²(dof) over windowscheck_spd, check_chi2_window
S6e_ekf_updategain-solve residual; Joseph P⁺ ⪰ 0; P⁺ ⪯ P⁻; δx finite; δx ⟂ unobservable directionscheck_solve_residual, check_psd, check_loewner_le, check_finite, check_no_update_along
S9_marginalizationP′ = principal submatrix of P; bookkeeping; ⪰ 0check_principal_submatrix, check_dimension, check_psd
S10_online_calibrationextrinsic ∈ SO(3); calibration covariance ⪰ 0check_so3, check_psd
End to endNEES/NIS over windows; ATE/RPEcheck_chi2_window, check_scalar

The S6e row’s last check is the observability question at the heart of the #212 over-confidence. A consistent filter can’t gain information about global position or yaw, so the update must have no component along those four directions.

Definiteness, Loewner order and rank use small Jacobi eigenvalue and SVD kernels (invariants/linalg.hpp). They are computed in the type under test and are accurate to relative precision, which an oracle needs, because the verdicts hinge on the smallest values.

#include <branes/sdk/eval/invariants.hpp>
namespace inv = branes::sdk::eval::inv;
using inv::Stage;
inv::InvariantReport rep;
rep.add(inv::check_solve_residual(K, S, PHt, Stage::S6e_ekf_update)); // K S = P Hᵀ
rep.add(inv::check_psd(P_post, Stage::S6e_ekf_update, "joseph.psd"));
rep.add(inv::check_loewner_le(P_post, P_prior, Stage::S6e_ekf_update)); // P⁺ ⪯ P⁻
rep.add(inv::check_no_update_along<double>(dx, N_unobs, Stage::S6e_ekf_update));
if (const auto bad = rep.first_failure())
std::cerr << to_string(bad->stage) << ": " << bad->name << " = " << bad->value
<< " " << bad->unit << " (threshold " << bad->threshold << ")\n";

A deliberately violating input shows what the native-unit residual buys you. An “update” that adds cc⊤cc^\top with ∥c∥2=0.30\|c\|^2 = 0.30 to the covariance fails loewner_le with value = 0.30 in matrix units: the size of the violation, not just a red light.