Skip to content

Match detections to where each aircraft is, not where it was - #76

Merged
Purple10101 merged 1 commit into
mainfrom
20261005-adsb-match-position-age
Oct 5, 2026
Merged

Purple10101 merged 1 commit into
mainfrom
20261005-adsb-match-position-age

Conversation

@Purple10101

Copy link
Copy Markdown

What

The detection-to-ADS-B matcher in api/server.js compared each detection against every aircraft's position as last reported. It now projects each aircraft to the detection's timestamp first. It uses the existing constant-velocity extrapolatePosition, once per frame.

Why

The matcher only worked while the feed was about 1 s fresh. Since retina-node v0.4.7.0 (2026-09-27), nodes take positions from adsb.retina.fm, which reach blah2-api 1-15 s old. Near the baseline an airliner's Doppler changes by about 5 Hz/s, so position age alone puts real aircraft outside the 5 Hz tolerance.

On jonathan-node-1:

  • Tagged tracks fell from about 230 a day (2026-09-06/07, median residuals 0.28 km / 1.4 Hz) to almost none.
  • An airliner passing at SNR 11-20 never matched. Its delay residual was under 1 km, but its Doppler residual was -4 to -23 Hz, always the same sign and growing with seen_pos.

Changes

  • lib/extrapolation.js
    • aircraftAtTimestamp() projects every aircraft to the detection time. It drops aircraft it can't project (too old, or no speed or heading, which bistatic.js needs for a Doppler anyway). Records with no position time are passed through unchanged.
    • The matcher allows positions up to 20 s old (MATCH_MAX_POSITION_AGE_S). The /api/adsb2dd overlay keeps its 10 s limit (MAX_EXTRAPOLATION_S). Half of adsb.retina.fm's positions were over 10 s old on jn1.
    • The barometric climb rate is passed through as geom_rate, the only rate field bistatic.js reads.
  • Position times now come from the payload's own now minus seen_pos (stampPositionTimes()). The tar1090 proxy sets now to its fetch time, so measuring from our own clock made cached positions look younger than they were. The overlay uses the same stamp.
  • Match records gain a position_age field (s). All existing fields are unchanged, but lat/lon/alt are now the projected position.

Testing

  • api/test_adsb_position_age.js, 28 checks.

    • It uses jn1's real geometry with a jet passing the node, its true position computed by an independent spherical formula.
    • The raw position misses by more than 5 Hz at 2 s and 5 s of age, as seen live. The projected position matches within 0.05 km / 0.5 Hz at 2-15 s.
    • It also covers the age limits, backwards projection, the climb rate, missing fields and payload now handling.
    • The existing API tests still pass: test_alt_baro_fallback, test_tar1090_extrapolation, test_bistatic, test_timestamp_units, test_tracker_forward.
  • Live side-by-side on jn1. The old and new matchers ran on the same 1200 frames and the same feed, read-only inside the running container:

    Old New
    Matched detections 4 140
    Aircraft matched 2 6
    Median abs Doppler residual 2.7 Hz 1.8 Hz

    One overflight (JBU385) matched continuously as its Doppler swept +158 to -182 Hz.

  • Deployed to jn1 as a hand-swapped hot-patch since 2026-10-05 15:15 UTC and confirmed working live. The overlay still places 82-85% of aircraft.

Known gaps

  • A small positive bias remains, about +1 Hz mean, flat across position age. That points to the detection timestamp sitting off the CPI centre rather than to the projection. It's well inside tolerance, so it's left alone.
  • In the side-by-side run, the old matcher matched one aircraft (EJA353) on 2 frames where the new one did not. It is probably past the 20 s limit, but that wasn't confirmed.
  • This conflicts with the unmerged 20261002-blah2-api-performance branch, which moves this matcher into lib/adsb_match.js. Whichever merges second needs a small rebase, and that branch's equivalence test will need updating, since this change alters matching deliberately.

Docs

None changed. blah2-arm has no prose docs covering the matcher (doc/ is generated Doxygen output only), so the reasoning lives in the code comments and this PR.

🤖 Generated with Claude Code

The detection-to-ADS-B matcher compared every detection against each
aircraft's position as last reported. With a feed about 1 s fresh that was
close enough; adsb.retina.fm positions reach blah2-api 1-15 s old, and near the
baseline an airliner's Doppler moves ~5 Hz/s, so the age alone put real
aircraft outside the 5 Hz tolerance. On jonathan-node-1 an overflight seen at
SNR 11-20 never matched, and tagged tracks fell from ~230 a day to almost none.

Each aircraft is now projected to the detection's timestamp before matching,
once per frame, using the existing constant-velocity extrapolatePosition. The
matcher allows positions up to 20 s old (the overlay keeps its 10 s), drops
records it cannot project, and passes the barometric climb rate through as
geom_rate so a climbing aircraft gets the right Doppler. Matches carry a new
position_age field.

Position times now come from the payload's own `now` minus seen_pos. The
tar1090 proxy sets `now` to when it fetched the data, so measuring seen_pos
from our own clock made every cached position look younger than it was. The
/api/adsb2dd overlay uses the same stamp.

Measured live on jn1, 1200 frames, old and new matcher run side by side on the
same frames and feed: 4 -> 140 matched detections, 2 -> 6 aircraft, one
overflight matched continuously as its Doppler swept +158 to -182 Hz.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Purple10101
Purple10101 merged commit fa12bf6 into main Oct 5, 2026
2 checks passed
@Purple10101
Purple10101 deleted the 20261005-adsb-match-position-age branch October 5, 2026 15:27
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.

1 participant