Skip to content

feat(eskf): optional vertical sigma and per-fix GNSS covariance (T2-0a) - #129

Merged
Telschow merged 3 commits into
mainfrom
feat/t2-0a-gnss-sigma
Oct 19, 2026
Merged

Telschow merged 3 commits into
mainfrom
feat/t2-0a-gnss-sigma

Conversation

@Telschow

Copy link
Copy Markdown
Owner

Closes #108

Three commits, from a fresh clone, branch from origin/main at 83370c9:

  1. 01ecf35, the stranded commit (796db17), cherry-picked with its author.
  2. 1897f2a, review: diagonal-only wording, tests for a tiny variance, the gate seeing the same R, the vertical sigma.
  3. the review fix for question c: unusable per-fix variances raise, small ones are floored (new field gnss_per_fix_min_sigma_m).

The second commit on the stranded branch (the UrbanNav Tunnel-1 run, a23512c) is not taken. That is T2-0b.

Cherry-pick conflict

One conflict, in tests/test_estimators.py. Both sides appended tests at the end of the file: main added the reset and transition-matrix tests, the stranded commit added three vertical-sigma and per-fix tests. I kept both, main's block first. eskf.py and docs/MODEL.md merged cleanly.

Golden file

Step 3, main's golden file, test run without --update (the script has no check mode, so this is pytest tests/test_golden_benchmark.py): 7 failed, 6 passed. The 7 failures are the 7 per-case tests, and each reports exactly 3 differences: the config_hash, gnss_per_fix_covariance and gnss_vertical_sigma_m ("unexpected (golden lacks it)"). No metric differs. The per-case test covers 7 of the 15 cases, so the other 8 are covered by the diff and by the run comparison below.

Step 4, after the review fix added the third key, --update, then git diff origin/main of the golden file, collapsed by line:

15 +    "gnss_per_fix_covariance": false,
15 +    "gnss_per_fix_min_sigma_m": 0.01,
15 +    "gnss_vertical_sigma_m": null,
15 +   "config_hash": "<hash>",
15 -   "config_hash": "<hash>",

That is exactly 15 hash replacements and three new keys per case. Nothing else.

Nothing moved

navkit run --out on origin/main (a separate worktree) and on this branch, with runtime_s, wall_s and realtime_factor stripped, compared path by path:

15 config_hash differs
15 gnss_per_fix_covariance only in branch
15 gnss_per_fix_min_sigma_m only in branch
15 gnss_vertical_sigma_m only in branch
total differing paths: 60

No other path differs. check_doc_tables.py passes without editing any table.

Answers

a. Defaults. With both settings at their defaults, _gnss_update receives sigma_vertical=None and variance=None, so neither if runs and R is the same eye(3) * sigma**2. In the run loop, own is None when gnss_per_fix_covariance is False, so a fix's cov is never read. When the setting is on and a fix has no cov, the sigmas are used. The golden file and the run comparison above confirm it on all 15 cases. Tests: a bad cov is ignored when the setting is off (the run equals the fixed-sigma run exactly), and the setting on with no cov equals the setting off.

b. Diagonal only. GnssFix.cov is (N, 3) variances, so a correlation between axes (east-north) cannot be expressed. Said in the EskfConfig comment and in docs/MODEL.md. No cross terms were added.

c. Zero, negative, NaN, inf. Before this PR the code did neither reject nor floor. Zero was accepted as an exact fix (the state jumped onto the measurement and the covariance on that axis went to zero). Negative, NaN and inf were rejected only by accident, as a gate rejection with an infinite distance, so FDIR would have counted a data defect as a faulty receiver. Behaviour chosen by the maintainer and now in place:

  • On the per-fix path only (variance given, which happens only when gnss_per_fix_covariance=True and the fix carries a cov), a negative, NaN or infinite variance raises ValueError inside _gnss_update, before anything is gated or fused. The message names the epoch and the axis index and says the value is not usable. It is not a gate rejection, nothing is logged for FDIR, and no counter was added.
  • Every other variance is raised to at least gnss_per_fix_min_sigma_m squared, per axis. The new EskfConfig field gnss_per_fix_min_sigma_m defaults to 0.01 m (1 sigma). It is validated finite and positive. Zero, a negative, NaN or inf floor raises ValueError when the config is built. The value 0.01 m is the maintainer's pick and it is a config field, so it is easy to change. It has no effect unless gnss_per_fix_covariance is on, so defaults and the benchmark numbers do not change.

d. Tiny variance. A variance of 1e-12 per axis is below the floor, so the update equals the floored one exactly, is accepted, has a finite Mahalanobis distance, and leaves P finite, symmetric and positive semidefinite.

e. Vertical sigma. It sets R[2,2] only. The innovation covariance the gate sees is built from the same Rcov as the update. A test checks S[2,2] = P[5,5] + sigma_v^2 and that the horizontal and off-diagonal terms are unchanged. A second test shows the declared vertical sigma alone decides whether a 12 m height jump is gated.

f. test_eskf_config_as_dict_covers_every_field passes. All three fields are in as_dict, so the config hash covers them.

g. The text added to docs/MODEL.md is in words and carries no measured number. The only numeral is the config default in the table, as for the neighbouring rows.

Tests added

Written first, shown failing on the earlier branch tip (22 failed: the new field missing, and "DID NOT RAISE" for the run-level bad-covariance test), now passing:

  • negative, NaN, -inf and inf, each on axis 0, 1, 2 and on all three: raises ValueError with the epoch, axis N and "not usable"; the state and covariance are unchanged after the raise (deep copy compared), and no innovation or bias record was appended;
  • zero on all three axes gives exactly the posterior state and covariance of a variance of floor squared;
  • a per-axis mix of zero, one and 1e-9 equals the floored vector (each axis at least floor squared);
  • a variance just above the floor (twice the floor in sigma) is used as given and differs from the floored update;
  • config: floor of 0, negative, NaN and inf raise; the default is 0.01;
  • run level: a bad cov with the setting off equals the no-cov run; with the setting on it raises; with the setting on and no cov it equals the setting off;
  • from the review commit: a tiny variance (now asserts equality with the floored update), the vertical sigma in the gate and in the update, the vertical sigma deciding a height jump.

For T2-0b (not changed here)

src/navkit/urbannav_data.py is on the stranded second commit and is not in this package. When it feeds the filter with gnss_per_fix_covariance=True, a fix with a missing or NaN covariance now raises ValueError (a missing cov array falls back to the sigmas, but a NaN entry raises). The reader must drop such fixes before they reach the filter. T2-0b has to check this.

Checks

ruff check, ruff format --check, mypy src, check_doc_tables.py (against the fresh benchmark, no table edited), metrics.py --check and check_links.py pass. The test count is regenerated. Full suite on the final tree: 1490 passed, 24 skipped, 2 xfailed in 576.38s (0:09:36). The coverage ratchet is left to CI.

Left

Nothing unresolved. The floor of 0.01 m is the maintainer's choice and is a config field.

🤖 Generated with Claude Code

Telschow and others added 3 commits October 19, 2026 10:58
…nerate golden

Two opt-in settings, both off by default: gnss_vertical_sigma_m (a separate
vertical sigma) and gnss_per_fix_covariance (weight each fix with its own
GnssFix.cov). Both are serialised in as_dict so the config hash covers them.

The golden snapshot changes only in the 15 config hashes and the two new null/false
keys. No number moves.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Say in the config comment and docs/MODEL.md that the per-fix covariance is a
diagonal and cannot carry a correlation between axes. Add tests that a very
small per-fix variance keeps the covariance symmetric and semidefinite, that
the gate sees the same R as the update, and that the declared vertical sigma
alone decides whether a height jump passes the gate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s (T2-0a)

On the per-fix path only, a variance that is negative, NaN or infinite raises
ValueError naming the epoch and the axis, before anything is gated or fused,
so a data defect is not counted as a faulty receiver. Every other variance is
raised to at least gnss_per_fix_min_sigma_m squared (new field, default 0.01,
validated finite and positive). Defaults and the benchmark numbers do not
change; the golden file gains the new key and the config hashes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@Telschow
Telschow merged commit 5f5abef into main Oct 19, 2026
20 checks passed
@Telschow
Telschow deleted the feat/t2-0a-gnss-sigma branch October 19, 2026 11:10
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.

Optional vertical GNSS sigma and per-fix GNSS covariance

1 participant