Follow a silent known aircraft onto new nodes and re-anchor after a stale prior - #334
Conversation
state.adsb_aircraft keeps an entry long after the transponder stops reporting, and _adsb_velocity read gs/track off it with no age check. For an aircraft the known lane is deliberately still solving from radar alone that is a heading from before the silence: when it turns, the filter's velocity keeps being corrected toward the OLD course (the ADS-B velocity is a recurring measurement update, sigma 5 m/s), the lane's dead-reckoned prior walks off along it, and the honest solves that follow are measured against that prior and labelled ghosts. Traced on one aircraft in a 15-min capture: the guess drifted 0.26 -> 1.0 km through a turn while every solve stayed within 0.4 km of truth. Both smoothers now age the fix against the SOLVE's own epoch (never wall clock, so a replay sees the same verdict) and fall back to the solved velocity past the same 45 s cap claiming uses. The constant comes from the library rather than from services.known_claiming, which reaches this module through services.dark_follow and would close an import cycle. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The kf seed is a prior, not a measurement: the lane's own last published position dead-reckoned forward. When the aircraft turns during its silence the prior keeps flying the old heading, displacement crosses the ghost threshold, and because a ghost is never published the prior stops being refreshed — so the gap grows without limit and every later solve is judged against a position the aircraft left minutes ago. A capture shows 25 consecutive ghosts, all within 0.6 km of truth, against a prior 13 km away; the aircraft never comes back. What separates a moved prior from a wrong solve is repeatability — a wrong solve is wrong somewhere new each time. So KNOWN_LANE_REANCHOR_STREAK (default 2) consecutive kf-seeded ghosts that agree with EACH OTHER to within the same displacement cap are published as label "reanchored". A fix-seeded ghost never re-anchors: a live transponder stays the lane's truth gate, and re-anchoring away from it would publish a position the aircraft's own fix contradicts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The hold only helps the nodes that already had the aircraft. A node that ACQUIRES one during its silence has no tag, no fresh fix and no hold, so its detections go straight to the dark pool: in the capture, four such nodes minted a dark twin 141 s into an outage, beside the entry the known lane was still publishing. The lane's own published position is the candidate those nodes are missing — a radar measurement of that aircraft, seconds old, on a key every node can read. _follow_states offers each mn-adsb-<hex> entry (at most KNOWN_FOLLOW_MAX_AGE_S old, KNOWN_FOLLOW_MIN_SOLVES solves behind it, velocity from the filter, world tag from the cache while it lasts) to path 2 as an ordinary cached state, so it goes through the existing machinery unchanged: visibility gate, range prescreen, age-scaled gates, Hungarian. Hexes with a FRESH fix are skipped — path 2 already has the better candidate and the same aircraft must not enter the assignment twice. The claim carries the hex's ORIGINAL stale fix (from any node's hold, since the fix is the aircraft's and not the node's) so the lane still reads the aircraft as silent and seeds from its own solve; plus follow: True, and the hold every claim creates, which is the point — from the next frame the new node holds the track on its own measurements. Counters known_follow_claims and known_lane_reanchored ride the solver-stats known_claims / known_lane blocks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two follow-ups from the full suite. The velocity guard rejected a fix whose timestamp sits AHEAD of the solve epoch as well as behind it, which is not a stale heading — it is a fresher one, and it is what clock skew between a node and the ADS-B feed (or a fixture whose solve timestamps are small synthetic numbers) actually produces. Only lateness is guarded now. And the two known_* solver-stats blocks are asserted key-for-key, so the new reanchored / follow_claims entries have to be named there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…the fix A follow candidate without a world tag passes the world gate on every node, and real traffic flies over the simulated fleet's footprint: the decoy case that gate exists for. Every hold stamps the claiming node's world, so a silent hex whose cache entry has been pruned still carries one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Claude finished @jehanazad's task in 2m 18s —— View job Reviewing PR #334
This sync added one new commit since the previous review: 60109cc — "Give a follow candidate the hold's world tag when the cache has lost the fix" ( New commit review The fix is correct and narrow: Two minor items:
Everything else in the commit — the Branch: |
Summary
Follow-up to the known-track HOLD (#333, this PR's base). The hold keeps a silent ADS-B aircraft claimed on the nodes that already had it, but a 15-minute A/B with transponder outages on half the fleet showed it is still shown as itself only ~54% of the time after 45 s of silence. Three causes, one per commit:
track_filter._adsb_velocityread gs/track off the cached ADS-B entry with no age check, so during a silence the filter's velocity kept being corrected toward the pre-silence course. When the aircraft turns, the lane's dead-reckoned prior drifts along the old heading (traced: 0.26 -> 1.0 km) while every solve stays within 0.4 km of truth._MAX_DISPLACEMENT_KMthe solve is labelled ghost, ghosts are not published, so the prior stops being refreshed and the gap only grows: 25 consecutive ghosts, all within 0.6 km of truth, against a prior 13 km away.Changes
services/track_filter.py,services/tasks/solver.py):_adsb_velocitytakes the epoch the velocity is wanted for (the solve's owntimestamp_ms, never wall clock) and returns "no ADS-B velocity" pastADSB_SEED_MAX_DR_AGE_S(45 s, the constantKNOWN_CLAIM_MAX_FIX_AGE_Saliases). Imported from the library, not fromknown_claiming, which reachestrack_filterthroughdark_follow.solver._ewma_smooth_trackwas the other unaged reader in a dead-reckoning path and now shares the helper.solver._gt_stamp's ADS-B fallback already ages its fix (_MLAT_HISTORY_ADSB_MAX_DT_S) and scores rather than steers, so it is unchanged.services/tasks/known_lane.py):_reanchorkeeps the last kf-seeded ghost per hex (60 s TTL, cleared by_reset_for_tests).KNOWN_LANE_REANCHOR_STREAK(default 2) consecutive kf-seeded ghosts that agree with EACH OTHER to within_MAX_DISPLACEMENT_KMare published as labelreanchoredand counted inknown_lane_reanchored. A fix-seeded ghost never re-anchors: a live transponder stays the lane's truth gate. 0 disables.services/known_claiming.py):_follow_statesbuilds synthetic path-2 candidates from publishedmn-adsb-<hex>entries for hexes whose fix is stale or gone — at mostKNOWN_FOLLOW_MAX_AGE_S(20 s) old,KNOWN_FOLLOW_MIN_SOLVES(3) solves behind them, velocity fromlearned_velocity, world tag from the cache while it lasts. They enter the existing path-2 machinery unchanged (visibility gate, range prescreen, age-scaled gates, Hungarian). The claim carries the hex's ORIGINAL stale fix, taken from any node's hold, so the lane still reads the aircraft as silent; plusfollow: Trueand the hold every claim creates, so the new node holds the track from the next frame. Counterknown_follow_claims;KNOWN_FOLLOW_MAX_AGE_S=0disables. Path H precedence and the consistency rule are untouched, anddark_follow.pyis not modified.known_lane/known_claimsblocks;docs/solverflow.mdextended beside the path-H paragraph, with the new constants in the table.Test coverage
New
backend/tests/test_known_track_follow.py(12 tests): a 60 s-old eastbound fix does not stop the filter learning a southward velocity while a fresh one still seeds it; two self-consistent kf-seeded ghosts publish asreanchored(counter, history record, feed entry) while a fix-seeded streak and two mutually inconsistent ghosts do not; a second node with no hold and no fresh fix claims a detection predicted from a 5 s-old 3-solve entry, carriesfollow=Trueand the original fix, and gains a hold — and nothing is offered for a too-old entry, a below-threshold solve count, a fresh fix, or with the path switched off.Review notes
_adsb_velocitystill uses an entry that carries no timestamp at all: no live writer produces one, and rejecting it would disable the velocity seed for pre-timestamp records rather than address the stale-heading case.KNOWN_LANE_REANCHOR_STREAK=0,KNOWN_FOLLOW_MAX_AGE_S=0) on top of the hold's ownKNOWN_HOLD_MAX_GAP_S=0.Live verification (test fleet, simulator transponder outages, 15-minute captures)
Same scene as the #333 A/B (
frac_adsb_outage0.5,frac_dark0 so every dark-lane key is a ghost of an ADS-B aircraft), third column is this branch on top of #333:The stale-velocity guard is what moved the last two rows: with it, every filter-seeded known-lane solve in the window was a truth match (45 of 45) and the "ghost forever" loop did not occur, so the re-anchor never had to fire (
known_lane.reanchored0). The follow path also recorded 0 claims in this window — its precondition (a published entry under 20 s old plus a node newly acquiring the silent aircraft) did not arise in 15 minutes with about three silent aircraft at a time — so it is covered by its unit tests only; the counterknown_claims.follow_claimswill show it working when it does. No claiming errors, bench-neutral paths untouched.🤖 Generated with Claude Code