Repository navigation
Calibrated, universal tracker: hold zero-Doppler crossings, measured R and Q, no min_snr - #37
Conversation
…crossing live_score fitted one slope to a track's delay and compared it with the track's mean Doppler. Across a zero-Doppler crossing the range is at an extremum, so both are near zero with signs that need not agree, and every track that held a crossing read as impossible. On a jn1 replay that put two ADS-B-labelled aircraft with clean crossings among the implausible tracks. Each side of a crossing is now judged on its own, and a track is impossible if either side is. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
blah2 blanks Doppler near zero, so an aircraft's detections change sign across a gap. blank_crossing_rate is the share of crossings, of those a track reached, where the same track holds the aircraft on the far side. It is the number the blank work is judged on, and it was 1-6% on the harness. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
blah2 never reports within process.detection.minDoppler of zero, and an aircraft crossing zero Doppler sits there for 4-90 s because its range is at an extremum. A confirmed track was deleted after 10 missed frames, 5 s, so nearly every crossing ended one track and started another. A miss the blank can explain is no longer counted towards deletion: the prediction is inside the blank, has crossed zero since the last detection, or its gate reaches into the blank. Once inside, the hold latches until the next association, because the acceleration estimated going in is too poor to time the exit. A track is held no longer than it had been observed, and never longer than 120 s. The observation limit is what keeps the hold from making false tracks. Without it, jn1 tracklets of 5-7 detections were held for 27-51 s and then took detections 3-11 km away. The blank is read from the node's blah2 config through --blah2-config. Unset means unknown, and then nothing is held. On a replay of 2.5 h of jn1: labelled crossings held 0 of 10 to 8 of 10, implausible tracks 1 to 1, tracks per aircraft 1.82 to 1.50. On the synthetic harness, crossings held 1-6% to 29-30%. Design and measurements are in docs/zero-doppler-blank.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… stop jumping A confirmed track would take one stray detection kilometres off its path and come straight back. R was a fixed 1 km of delay noise, about 7 cells, and the SNR scaling took it past 2 km at the detection floor, so the gate was about +/-6 km for exactly the detections most likely to be false. Real scatter is a small fraction of a cell: consecutive steps in jn1 tracks miss constant acceleration by a median 15 m. R is now one resolution cell on each axis, c/fs by 1/cpi from the node's own capture. Innovations from jn1 and fairforest B both put R 25-33x too large in delay but disagree on a measured k by 3.7x, so one cell is the conservative value that needs no fitting. Q gains a range-rate random walk, process_noise.range_rate = 1e-5 km^2/s^3, because a tighter R without it loses aircraft where Doppler slews. The ADS-B start covariance squares its position setting. It was a standard deviation used as a variance, which would now be looser than a radar-only start. Replayed on three nodes under their own configs, jumps over 1 km went from 24 to 0 on jn1, 328 to 27 on fairforest B and 30 to 0 on jn2 (UHF), with live_score's implausible tracks falling at all three. No real aircraft was lost from tracking; the "aircraft" that went were a wide-gate artefact on jn1 and CW-tone tracks with coincidental ADS-B labels on fairforest B. One real jn1 crossing was lost. On the harness, ID switches halved and clutter absorption fell from 10-12% to 3-4%. Fourteen tests failed on fixtures that were physically impossible, mostly a delay moving against its own Doppler, which the old gate accepted. They now move as their own Doppler says, and the intermittent ADS-B test that was a strict expected failure for that reason now passes. Design and measurements are in docs/measurement-noise.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two goals, agreed with Josh: an ADS-B-verified aircraft shows as a single track, and the filter only has to model well-constrained physics, leaving dramatic changes of velocity or direction to stitching after the fact. The second is a design rule for the filter: it is why R can be one resolution cell and why Q should not be raised to catch manoeuvres. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ratch Nine of its ten checks judged ADS-B rather than the radar, so a target with no transponder got one fixed-threshold test. On live data it only caught the tracker's own mistakes: over 34.5 hours of v0.4.0 on jn1 its four flags were all identity swaps from tracks that had jumped onto a second aircraft, and with the gate sized from the resolution cell it flagged nothing in 8.8 hours. Removed: every per-track check and its state, their thresholds, the synthetic scorer's anomaly metrics, the anomaly test suites and the integration compose file, which existed to confirm anomaly flags and no longer ran. The wire is unchanged, because retina-telemetry requires the fields on every track: is_anomalous is always false, anomaly_types always empty, and max_velocity_ms is now the radar's speed floor, |range rate| / 2, which holds in any bistatic geometry. A track's adsb_hex no longer switches when a second aircraft's hex appears on it. The reasons and the rebuild proposal are in docs/anomaly-detection.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
R was one resolution cell, a conservative bound. Recorded for 26 hours on jn1, fairforest B and jn2 under the same code, and measured from smoother residuals of ADS-B and physics-checked tracks, delay noise is a tenth of a cell on every node and Doppler noise is about 0.4 m/s of range rate on both VHF and UHF, 2.2x apart in bins. Delay variance falls as SNR^-0.4, not SNR^-1; Doppler noise barely moves with SNR. Delay noise is now 0.12 cells at 10 dB, scaled by max(SNR/10 dB, 0.1)^-0.4; range-rate noise is a fixed 0.4 m/s. One function scales it for both the update and the gate, which had carried its own copy of the formula. A track's start no longer takes ADS-B's 100 m position uncertainty: its state always came from the radar detection, whose measured noise is 18 m. The adsb.initial_covariance setting goes with it. Six test fixtures moved their delay against their own Doppler, jumped back to their start, or sat 80 m off their true position; the old 150 m floor hid it. They now move as their Doppler says. Not shippable alone: Q is corrected in the next commit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…en per site With R measured, each node's ADS-B tracks were replayed offline over a grid of white jerk and range-rate random walk, choosing the Q whose median NIS sits at 2 ln 2 across prediction intervals. Every node's best fit had no range-rate walk: it had been compensating for an R ten times too large, so it goes. White jerk fits at 1e-6 to 3e-6 km^2/s^5 at far-traffic jn2 and 1e-5 at close-traffic jn1 and fairforest B. The floor becomes 1e-6, and the adaptive scaling, which widens Q by however far the filter's own NIS says the model is failing, carries it to each site's value with nothing set per site. It never engaged before, because the oversized R kept NIS near zero. No single Gaussian Q matches every interval: right at one frame, overconfident at 1-6 s, overcautious beyond. Manoeuvres are left to adaptive widening and to stitching rather than widening every gate for every track. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tracker.min_snr set detections below a hand-set SNR aside before the tracker saw them: 4 dB on jn1 and jn2, 7 dB on fairforest B. That is a per-site tuning, and with measured noise the gate and the physics are what reject noise, so it goes. Low-SNR detections still carry more delay noise, by the measured SNR^-0.4 scaling, so the gate admits them less readily. A non-finite SNR is now rejected and counted: it used to be routed into below_snr, and with no threshold it would reach the covariance. below_snr stays on the wire, always empty, because retina-gui draws it as a layer. A config that still sets min_snr loads and is ignored, checked against the tracker and its CLI. retina-gui's config editor and retina-node's default.yml still carry the field; that is follow-up work there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ation tool An innovation record gave the prediction interval only as dt * (1 + n_missed), and frames held through the zero-Doppler blank are not misses: 2-4.5% of the October records were off by up to two minutes. Each record now carries interval, the time since that track's previous update. retina_tracker.calibrate extrapolated innovation scatter to zero interval to separate R from Q. At 2 frames per second nothing is shorter than one frame and scatter rises by half between 0.5 s and 1 s, so its intercept moved from 2 m to 18 m with the choice of tracks. It ran only offline and nothing imported it, so it goes. docs/measurement-noise.md now describes the method that worked: smoother residuals for R and a replay fit for Q. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replayed on jn1, fairforest B and jn2, the measured noise as R lost 5-7% of ADS-B-labelled detections against the deployed code, and raising Q recovered little of it. The widths were robust estimates of the typical detection, and real radar noise has heavy tails: at those widths the chi-squared gate of 9, which should admit 98.9% of genuine measurements, refused 7.7-12.2%. The scale that makes the gate admit 98.9% of real aircraft measurements was 2.00x, 2.05x and 2.10x at the three nodes, on both bands, so R is now twice the robust width: 0.25 cells of delay at 10 dB and 0.8 m/s of range rate. Against the deployed code on the same 4 hours per node, jumps over 1 km fall 6 -> 4, 8 -> 4 and 4 -> 2, crossings and fragmentation hold, and recall is 99.5%, 98.8% and 95.0% against 100%, 99.9% and 100%; the aircraft no longer mostly tracked are a 454 m jump the gate is right to refuse, a one-second three-detection appearance and a sparse fast-slewing target. Adaptive widening of Q did not engage with this R; a 1e-6 floor did as well as higher values, which brought jumps back. The comments now say so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Live on jn1, a track held through the zero-Doppler blank for 30 s or more occasionally took an unrelated detection when it reacquired, because its gate grows for as long as it is held. The constant-acceleration check that would refuse them, and its replayed cost, are written down for the stitching work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude finished @Purple10101's task in 23s —— View job Review of PR #37I read the core diff ( What looks right
Points to consider
Verdict: sound approach with good measurement backing. Points 1, 3 and 4 are worth a look before merge. |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude finished @Purple10101's task in 25s —— View job Review of PR #37I read the core diff ( Checked and correct
Points to consider
Verdict: the approach is sound and well backed by measurement. Points 1 and 2 are worth a look before merge. Point 4 conflicts with a repo rule. |
…hat no end-to-end test remains Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Claude finished @Purple10101's task in 24s —— View job Review of PR #37The Checked and correct
Points to consider
Verdict: the approach is sound and well backed by measurement. Point 1 is worth a decision before merge. Point 3 conflicts with a repo rule. |
Summary
Makes the tracker's filter physically honest and the same on every node. Tracks now survive zero-Doppler crossings, stop jumping kilometres, and run on noise values measured from real recordings instead of guesses. The last per-site setting (
min_snr) is gone, and the legacy anomaly detection is removed so it can be rebuilt on the improved tracker.Measured on jonathan-node-1 (VHF), fairforest B (VHF) and jonathan-node-2 (UHF) by replaying each node's own recorded detections through old and new code.
What changed
process.detection.minDoppler.Rmeasured on three nodes. 26 hours of innovation recordings, measured from smoother residuals: delay noise is a tenth of a resolution cell everywhere, Doppler noise about 0.4 m/s of range rate on both bands. Real noise has heavy tails, soRis twice the robust width (0.25 cells at 10 dB, 0.8 m/s), the factor every node needed for the gate to admit 98.9% of real measurements. Delay noise scales as SNR^-0.4, as measured.Qrefitted by replaying recorded aircraft tracks: white jerk with a 1e-6 floor; the range-rate random walk added on 5 October is removed.min_snr. Every detection with a finite SNR is tracked; non-finite SNR is rejected and counted.blank_crossing_rate; innovation records carry the true prediction interval;retina_tracker.calibrateis retired (its method cannot separateRfromQat 2 frames/s).Measured effect
4 hours per node, identical input, v0.4.0 → this PR:
Live on jn1 since 12 October.
Compatibility
events.jsonl,GET /frameand the history stream keep every field retina-telemetry and retina-gui read:is_anomalousis always false,anomaly_typesalways empty,max_velocity_msis the radar speed floor|range rate| / 2, and thebelow_snrdetection class is always empty.tracker.min_snr,adsb.initial_covarianceandprocess_noise.range_rateare removed; a config that still sets them loads and ignores them (checked with the CLI).process_noise.range_jerkdefault 1e-7 → 1e-6.Known limitations and follow-ups
docs/zero-doppler-blank.md, for the stitching phase.min_snrin its config editor and draws an always-empty "below SNR" layer; retina-node'sdefault.ymlstill setsmin_snr.Docs
docs/zero-doppler-blank.md,docs/measurement-noise.mdanddocs/anomaly-detection.md(new), plus README, CLAUDE.md and the config comments.Verification
ruff check, ruff format --check and pytest (364 passed) clean on every commit; vulture unchanged.
🤖 Generated with Claude Code