Frame the map on the receiver, and throw out what never happened
A first real capture came back as a map spanning 240 degrees north to 20 south, with the aircraft an indistinguishable smudge in one corner. Two separate faults, one of them mine from the start. A position is sent as half a position -- an even frame and an odd one -- and the pair only means anything while the aircraft has not moved between them. The registry kept the last of each forever and paired them regardless of age, so an even frame from ten minutes ago decoded against a fresh odd one to a place on the wrong side of the world. Measured: a pair 300 seconds apart puts the aircraft 2,566 nm from where it is, and one night's log had it happening to two aircraft in three, with eleven positions off the planet altogether. A pair is now good for ten seconds, the answer has to be on Earth, and the aircraft has to have been able to reach it. --radius, defaulting to a hundred, frames the picture on the receiver rather than on whatever was heard, so the scale is the same from one evening to the next. In the same unit as the speeds. The centre is the median of everything heard, which a handful of wrong positions cannot move, or --at LAT,LON says where the aerial is. --recheck repairs a log recorded before all this: for each aircraft it keeps the longest run of positions that could describe one aeroplane. Not a forward walk dropping whatever disagrees with the last position kept -- that lets one bad fix become the reference, and on the same log it discarded a fifth of everything, most of it the truth. Two calibrations came from the recording rather than from taste. The failures separate cleanly -- a hundred artefacts under a mile, twelve hundred real errors over fifty, and nothing in between -- because positions are stamped to the millisecond, so two a thousandth of a second apart imply thousands of knots across a few yards. Nothing under two miles is called an error. Afterwards the worst surviving jump is 513 kt. One rendering bug the tighter frame exposed: an aircraft just off the top left painted 743,774 pixels of an 844,200-pixel picture, because the dot at a marker's centre clamped its near edge and left the far one alone, and numpy reads a negative slice end as counting back from the far side. And a still of a whole evening was dead-reckoning every aircraft forward to the final moment, which for a ten-hour log flew 291 of 335 clean off the picture; a still now draws each where it was last actually heard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
This commit is contained in:
parent
96fc21ac7d
commit
e50d43d6e2
13 changed files with 999 additions and 36 deletions
|
|
@ -392,3 +392,107 @@ def test_a_payload_that_says_what_it_is_is_named():
|
|||
def test_an_ordinary_payload_claims_no_format():
|
||||
bits = "".join(format(b, "08b") for b in b"\x01\x02\x03\x04\x05\x06\x07\x08")
|
||||
assert not [r for r in interpret(bits) if r.kind == "fields"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# A position is only as good as the pair it was decoded from
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _position_frame(icao, lat, lon, odd, when, altitude=35000):
|
||||
"""One position frame, read back the way the decoder would read it."""
|
||||
from bandsaunter.adsb import _read, encode_position
|
||||
|
||||
data = encode_position(icao, lat, lon, altitude, odd=odd)
|
||||
frame = _read("".join(format(b, "08b") for b in data), data)
|
||||
frame.received_at = when
|
||||
return frame
|
||||
|
||||
|
||||
def test_a_stale_pair_is_not_a_position():
|
||||
"""Compact position reporting sends a fraction of a zone, so an even
|
||||
frame from ten minutes ago read against a fresh odd one puts the
|
||||
aircraft on the wrong side of the world. Measured against one night's
|
||||
recording it was doing exactly that to two aircraft in three."""
|
||||
registry = AircraftRegistry()
|
||||
registry.add(_position_frame(0xABCDEF, 32.55, -111.16, False, 1000.0),
|
||||
when=1000.0)
|
||||
registry.add(_position_frame(0xABCDEF, 33.22, -111.16, True, 1300.0),
|
||||
when=1300.0)
|
||||
assert not registry.aircraft["ABCDEF"].located
|
||||
|
||||
|
||||
def test_a_fresh_pair_still_places_it_exactly():
|
||||
registry = AircraftRegistry()
|
||||
registry.add(_position_frame(0xABCDEF, 33.22, -111.16, False, 1000.0),
|
||||
when=1000.0)
|
||||
registry.add(_position_frame(0xABCDEF, 33.22, -111.16, True, 1000.5),
|
||||
when=1000.5)
|
||||
craft = registry.aircraft["ABCDEF"]
|
||||
assert craft.latitude == pytest.approx(33.22, abs=0.01)
|
||||
assert craft.longitude == pytest.approx(-111.16, abs=0.01)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("gap", [0.0, 0.5, 4.0, 9.5])
|
||||
def test_a_pair_inside_the_allowed_gap_is_used(gap):
|
||||
registry = AircraftRegistry()
|
||||
registry.add(_position_frame(0xA0B1C2, 51.5, -0.12, False, 100.0),
|
||||
when=100.0)
|
||||
registry.add(_position_frame(0xA0B1C2, 51.5, -0.12, True, 100.0 + gap),
|
||||
when=100.0 + gap)
|
||||
assert registry.aircraft["A0B1C2"].located
|
||||
|
||||
|
||||
def test_a_position_that_is_not_on_earth_is_refused():
|
||||
"""A latitude of 240 degrees is not a place. One night's log held
|
||||
eleven of them."""
|
||||
from bandsaunter.adsb import _on_earth
|
||||
|
||||
assert not _on_earth(239.6, -111.0)
|
||||
assert not _on_earth(45.0, 200.0)
|
||||
assert _on_earth(-33.9, 151.2)
|
||||
|
||||
|
||||
def test_an_aircraft_cannot_cross_a_continent_between_two_frames():
|
||||
"""A position needing nine hundred thousand knots to reach is not a
|
||||
position, whatever the checksum said about the frames it came from."""
|
||||
registry = AircraftRegistry()
|
||||
for odd in (False, True):
|
||||
registry.add(_position_frame(0xA0B1C2, 51.5, -0.12, odd, 100.0),
|
||||
when=100.0)
|
||||
craft = registry.aircraft["A0B1C2"]
|
||||
assert craft.located
|
||||
was = (craft.latitude, craft.longitude)
|
||||
# Two seconds later, a pair that decodes to the far side of the Atlantic.
|
||||
for odd in (False, True):
|
||||
registry.add(_position_frame(0xA0B1C2, 40.7, -74.0, odd, 102.0),
|
||||
when=102.0)
|
||||
assert (craft.latitude, craft.longitude) == was
|
||||
|
||||
|
||||
def test_an_aircraft_that_really_moved_is_still_followed():
|
||||
"""The bar has to be above anything that flies, or a fast aircraft is
|
||||
called an error."""
|
||||
registry = AircraftRegistry()
|
||||
for odd in (False, True):
|
||||
registry.add(_position_frame(0xA0B1C2, 51.5, -0.12, odd, 100.0),
|
||||
when=100.0)
|
||||
# Sixty seconds on and eight miles further east: 480 knots.
|
||||
for odd in (False, True):
|
||||
registry.add(_position_frame(0xA0B1C2, 51.5, 0.08, odd, 160.0),
|
||||
when=160.0)
|
||||
assert registry.aircraft["A0B1C2"].longitude == pytest.approx(0.08,
|
||||
abs=0.02)
|
||||
|
||||
|
||||
def test_a_gap_in_reception_is_not_treated_as_an_error():
|
||||
"""Nothing heard for an hour, then a position a long way off: that is an
|
||||
aircraft that flew away and came back, not a bad decode."""
|
||||
registry = AircraftRegistry()
|
||||
for odd in (False, True):
|
||||
registry.add(_position_frame(0xA0B1C2, 51.5, -0.12, odd, 100.0),
|
||||
when=100.0)
|
||||
for odd in (False, True):
|
||||
registry.add(_position_frame(0xA0B1C2, 48.8, 2.3, odd, 4000.0),
|
||||
when=4000.0)
|
||||
assert registry.aircraft["A0B1C2"].latitude == pytest.approx(48.8,
|
||||
abs=0.05)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue