Live ADS-B: ground rows retire aircraft, altitude jump guard, fix-age expiry - #15
Conversation
… expiry adsb.retina.fm (/v2/point) is not trustworthy row-for-row, and the fleet was applying it verbatim. Verified on test/staging 2026-09-10/11 against solve mnece44394c3 (= N6389R, a PA-28): * Another aircraft's cruise altitude turns up in a low GA row's alt_baro (+23k…+33k ft over truth, flipping between polls, persisting up to ~50 s). The altitude the server's known lane pinned its solve to was +2/+8/+10 km wrong for two minutes. Every row is type "other" with no alt_geom, so there is no feed field to filter on — only scale separates the cases: a real aircraft moves under 150 m in a 5 s poll even at 6,000 ft/min. * Ground is encoded as a numeric 0, not adsb.lol's "ground" sentinel, so the parser dropped landed aircraft as unparseable rows instead of reading them as the one positive "it landed" signal the feed gives us. A landed aircraft kept flying east for 146 s (dead-reckoned by seen_pos, then a 60 s coast). * Expiry keyed on the last poll that mentioned an aircraft, not on its last fix, so the feed echoing a stale row kept a ghost alive ~115 s. Parser: ground rows (the "ground" sentinel or any non-positive altitude) are now emitted with on_ground=True and alt_baro 0.0; rows with no position or no usable altitude are still dropped. World ingest: an on_ground row retires a known aircraft at once (new "landed" stat) and never creates an unknown one; a row whose raw fix age already exceeds live_stale_s is skipped entirely; live_seen_s is the fix time on the world clock, so an aircraft leaves live_stale_s after its LAST FIX (~115 s → ≤ 60 s); and an updated aircraft whose feed altitude disagrees with its coasted one by more than 1 km keeps the coasted altitude (new "alt_rejected" stat) while still taking the row's position, velocities and baro_rate — until the disagreement has stood for 60 s, which is a re-level rather than a glitch and is adopted. A persistently wrong feed altitude is therefore bounded to a minute of wrongness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The altitude jump guard adopted ANY reading once the run of rejections had stood for 60 s, without checking that the rejected readings agreed with each other. Replaying N6389R (2026-09-10) the feed disagreed with the coasted altitude for 97 s straight, but across four different corrupt values (+10.0 km for 22 s, +2.1 km for 7 s, +9.9 km for 48 s, +8.0 km for 13 s, then truth). The old rule adopts +9.9 km at the 60 s mark and then rejects the returning truth as a fresh >1 km jump for another minute: 84 s wrong, versus 97 s unguarded — the guard barely pays for itself. Track the last rejected altitude (live_alt_reject_km) and restart the run whenever a reading disagrees with it by more than _LIVE_ALT_JUMP_KM, so only a run that agrees with itself can ever be adopted. That keeps the path the adopt rule exists for — a wrong HELD altitude from a corrupt spawn or a corrupt adoption, where the feed's consistent truth is the side being rejected — and drops the one that made things worse. A genuinely re-levelling aircraft never jumps a kilometre between polls, so it tracks the coasted altitude and never opens a run at all. Tests: the N6389R replay shape (four corrupt values, 100 s, all rejected, nothing adopted, truth accepted the moment it returns) and corrupt-spawn recovery (consistent truth adopted at the 60 s mark, not before); the existing sustained-run adoption test still passes unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Review follow-up (556f9c7): the altitude jump guard now adopts only from a self-consistent run of rejections. Defect: the adopt rule fired on elapsed time alone — any reading was adopted once Fix: new Tests (extend
|
Why
adsb.retina.fm(/v2/point, adsb.lol-shaped) is not trustworthy row-for-row, and the fleet applied it verbatim. Verified on test + staging 2026-09-10/11 against solvemnece44394c3(=mn-adsb-a85f17= N6389R, a PA-28):alt_baro— observed +23k…+33k ft over truth, flipping between polls, persisting up to ~50 s. The altitude the server's known lane pinned its solve to was +2 / +8 / +10 km wrong for two minutes. Every row istype: "other"with noalt_geom, so there is no feed field to filter on; only scale separates the cases — a real aircraft moves under 150 m in a 5 s poll even at 6,000 ft/min, the corruption is 7–10 km.alt_baro: 0, not adsb.lol's"ground"sentinel, so the parser dropped landed aircraft as unparseable instead of reading the one positive "it landed" signal the feed gives us. A landed aircraft kept flying east for 146 s (rows dead-reckoned byseen_posup to 55 s, then a 60 s coast).What
live_adsb.parse_point_responsealt_barois"ground"or any non-positive number is now emitted withon_ground: Trueandalt_baro: 0.0; everything else getson_ground: False. Rows with no position or no usable altitude are still dropped. Docstring says why.world.SimulationWorld.ingest_live_aircrafton_ground→ retire. A known hex is removed fromlive_aircraftandaircraftimmediately (the way_expire_live_aircraftdoes) and counted under the new"landed"stat; an unknown hex is skipped, never created.now_wall - captured_at) exceedslive_stale_sis not applied and does not create — a fix that old is no evidence the aircraft is where extrapolating it would put it. The existing clamp still covers ages inside the window.live_seen_s = self._time - age(the fix's capture time on the world clock) in both branches._expire_live_aircraftis unchanged; the effect is that an aircraft leaveslive_stale_safter its last fix rather than after the feed last echoed a stale row (~115 s → ≤ 60 s). Field comment and_expire_live_aircraftdocstring updated.ac.alt_kmhas been coasted to the world clock by_dead_reckonand the row'salt_kmextrapolated tonow_wallbyvel_up * age, so they are comparable. A disagreement over_LIVE_ALT_JUMP_KM = 1.0keeps the coasted altitude and counts"alt_rejected", while lat/lon/velocities/heading/vel_upare still applied (baro_ratewas correct in every corrupt row observed). The first rejection time is kept in a newSimulatedAircraft.live_alt_reject_s; once the disagreement has stood for_LIVE_ALT_ADOPT_S = 60.0it is adopted and the field cleared — a reading that disagrees for a full minute is a re-level, not a glitch, and a persistently wrong feed altitude is bounded to a minute of wrongness rather than forever. Any accepted reading clears the field._dead_reckonis untouched. The returned stats dict gains"landed"and"alt_rejected"(the two existing exact-dict assertions in the tests were updated).Tests
tests/test_live_adsb.py:"ground", numeric0and a negative altitude →on_ground True/alt 0.0; positive →False; missing/Nonealtitude still dropped.landed == 1) and never creates an unknown one.live_stale_screates nothing, updates nothing, and does not bumplive_seen_s.alt_rejected == 1, lat still updated), 1.2 (accepted, reject field cleared); a sustained 11.2 is adopted after 60 s of world time; a normal 0.1 km-per-poll descent is never rejected.Ran (borrowed venv, this package on
PYTHONPATH):Pre-commit on the changed files (
ruff,ruff format,dead-code,ruff-config): all passed.🤖 Generated with Claude Code