Repository navigation
feat(eval): integrated autocorrelation time and effective sample size (T2-1a) - #134
Merged
Merged
Conversation
… (T2-1a) Add integrated_autocorrelation_time and effective_sample_size to navkit.eval.statistics, with Sokal automatic windowing (FFT autocovariance, NumPy only) and a small frozen result type that says whether the window rule was met. Tau is clipped at one and n_eff to [1, n]. Known-answer tests: white noise, AR(1) at three coefficients against (1 + a) / (1 - a), chi-square(3), a slow AR(1) plus white noise, a random walk and a trend, scale and offset invariance, the refusals, and a property test. Nothing uses the estimator yet. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #112 (not closing it). No wiring: no verdict, table, golden file or page changes, and the estimator is not used anywhere yet.
navkit run --outonorigin/mainand on this branch is identical after strippingruntime_s,wall_sandrealtime_factor(15 cases; the cleaned JSON is the same size and compares equal).What it adds
navkit.eval.statistics(exported fromnavkit.eval):integrated_autocorrelation_time(x, c=5.0) -> floateffective_sample_size(x, c=5.0) -> floatautocorrelation_estimate(x, c=5.0) -> AutocorrelationEstimate, a frozen result withtau,window,truncated,n,c, for a caller that wants to know whether the window rule was met. The two public functions return plain floats, as specified.Method, and why Sokal windowing
Remove the mean. Autocovariance by FFT, zero-padded to a power of two of at least 2n (NumPy only).
rho_kis the autocovariance at lag k over the one at lag zero (the biased estimator, sums over n, which keeps the estimated sequence positive semidefinite).tau(M) = 1 + 2 * sum_{k=1..M} rho_k. The window is the smallest M withM >= c * tau(M), searched up ton // 2. If none meets the rule, the value atn // 2is returned andtruncatedis set.n_eff = n / tau, clipped to[1, n], and tau is clipped at 1 (antithetic series are not rewarded; the docstring says so).Why windowing: a fixed window cannot work. Too short cuts off a slow tail; too long adds the noise of the sample autocorrelation at large lags, which does not shrink with the lag. The rule grows the window with the answer. Method as decided; not changed.
Refusals, each with a test: fewer than 10 samples, NaN or inf, a constant series, a non-1-D input, a non-positive
c.AR(1) spread I measured, and the tolerances I chose
tau = (1 + a) / (1 - a)exactly forx_t = a x_{t-1} + e_t. Relative deviation of the estimate from the closed form over eight seeds (100 to 107), scratch script, no series truncated:Each tolerance is at least about 3 sd, and about twice the largest deviation seen; the bias is under 2% in all cases, so the spread, not a bias, sets the tolerance. a = 0.99 gets its own n (2 million) as asked; the test seeds (11, 12, 13) are different from the measured ones. The effective sample size is checked at 1.5 times the tolerance.
Other known-answer tests: white noise (tau within 0.05 of 1), chi-square(3) i.i.d. (a consistent NEES; within 0.06 of 1), slow AR(1) plus white noise (tau between the two pure cases and equal to
1 + (tau_AR - 1) * sharewithin 12%, a closed form and not just an ordering), scale and offset invariance (3x + 7and-x), tau never below 1 for an antithetic series, determinism, and thatcmoves the window.Non-stationary series. A trend never meets the rule: it is cut at
n // 2, marked truncated, and has a small n_eff. A random walk is the surprise: for the draws I used the rule is met late (a window of thousands of lags, a tenth of the length or more), sotruncatedis not set, yet tau is large and n_eff is a few samples. The test asserts the result over six draws (tau over 100, n_eff under n/100) and does not ask for a flag. Callers must not readtruncated=Falseas proof of stationarity.Property test:
n_effin[1, n], tau at least 1, both finite, for any AR(1) series of 10 to 400 samples withain (-0.99, 0.99) (Hypothesis, already a dev dependency), plus a seeded loop of heavy-tailed, step, trend, periodic and random-walk series.MEASURE (MEASUREMENT, synthetic; not field performance)
Every case in
configs/benchmark.yamlthroughnavkit.benchmark.run_case(astests/test_observability.pydoes, keeping the estimator result). The series is the per-epoch NEES the calibration report scores:normalized_error_squared(est.positions, ref_on_est.positions, position_cov), 3 degrees of freedom, all 6001 epochs. The outage rows use the epochs inside the configured outage window, the half-openwindow_maskthe report uses. The window rule cannot see lags past half the series, so in a 3000-epoch outage window it stops at 1500.truncmarks a window cut by that limit.Does the audit's "roughly 5 to 13 effective samples per 6001-epoch run" hold? Roughly, as an order of magnitude: single digits to low tens. Over the 14 scored whole-run series n_eff is 3.8 to 25.9; 11 of 14 fall between 11 and 18, and the two vision-only anchor rows are 3.8 and 3.9. So "5 to 13" understates the top (
gnss_onlyis 25.9) and the bottom. In every case the run carries a few to a few tens of independent samples out of 6001, which is what matters: the verdict that treats epochs as independent is overconfident by a factor of hundreds. Caveats: the NEES series of the divergent cases (the anchor rows, the vision-only rows, the outage-window series of every outage case) are not stationary, and wheretruncis yes the tau is a lower bound set by the window limit, so n_eff there is an upper bound. The window-only n_eff is also affected by there being fewer epochs than the whole run: 1500 is half of 3000.Mutation checks (not committed)
Window always the largest allowed (
window = taus.size - 1):(the AR(1) known-answer tests fail for all three coefficients). Mean subtraction removed (
centred = arr):Both restored; all tests pass on the unmutated code.
Checks
ruff check,ruff format --check,mypy src,metrics.py --check(test count regenerated, 1629),check_doc_tables.pyandcheck_links.pypass. The new file runs in about 1 s. Full suite:1598 passed, 29 skipped, 2 xfailed in 636.10s (0:10:36)(the skips are the dataset-dependent tests; the datasets are absent in this clone). The coverage ratchet is left to CI.🤖 Generated with Claude Code