ADS-B Entity Model
Table of Contents
Core Principle: Everything Changes
Aviation data is fundamentally temporal:
- Aircraft change registration
- Registrations change transponder codes
- Airports change codes
- Airlines merge, rename, cease operations
- Flight numbers are reused daily
Rule: Every relationship has valid_from and valid_to timestamps.
Entity Hierarchy
┌─────────────────────────────────────────────────────────────────────────────┐ │ │ │ IMMUTABLE (Physical) │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ AIRCRAFT │ │ AIRPORT │ │ │ │──────────────│ │──────────────│ │ │ │ serial_number│ ◄── Only truly fixed │ coordinates │ │ │ │ manufacturer │ identifier │ name │ │ │ │ model │ │ facility_type│ │ │ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ 1:many (changes over time) │ 1:many │ │ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ IDENTITY │ │ AIRPORT_CODE │ │ │ │──────────────│ │──────────────│ │ │ │ registration │ │ code (ICAO) │ │ │ │ valid_from │ │ valid_from │ │ │ │ valid_to │ │ valid_to │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ │ │ 1:many │ │ ▼ │ │ ┌──────────────┐ │ │ │ TRANSPONDER │ │ │ │──────────────│ │ │ │ mode_s_hex │ ◄── What we see in ADS-B │ │ │ valid_from │ │ │ │ valid_to │ │ │ └──────────────┘ │ │ │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ OBSERVED (What we receive) │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ SIGHTING │──────►│ POSITION │──────►│ FLIGHT │ │ │ │──────────────│ │──────────────│ │──────────────│ │ │ │ mode_s_hex │ │ coordinates │ │ identifiers │ │ │ │ timestamp │ │ confidence │ │ route │ │ │ │ receiver_id │ │ spoof_flag │ │ timestamps │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
The Identity Resolution Problem
What we OBSERVE (ADS-B): What we WANT TO KNOW:
═══════════════════════ ═════════════════════
mode_s_hex: "A12345" ────────► Which AIRCRAFT is this?
callsign: "UAL123" ────────► Which FLIGHT is this?
position: 42.3, -71.0 ────────► Is this REAL or SPOOFED?
RESOLUTION CHAIN:
mode_s_hex ──► TransponderAssignment ──► Aircraft ──► Registration
│ │
│ └──► Current Operator
│
└──────► Sighting ──► Flight (if callsign matches schedule)
The Stale Transponder Problem
┌─────────────────────────────────────────────────────────────────────────────┐ │ │ │ REALITY: │ │ │ │ Aircraft Serial: 12345 │ │ Current Registration: N67890 (since 2020-06-16) │ │ Current Mode S (per FAA): A00003 │ │ │ │ WHAT WE SEE IN ADS-B: │ │ │ │ Mode S: A00002 (OLD! From previous registration!) │ │ Callsign: "N67890" (pilot entered current registration) │ │ │ │ MISMATCH DETECTED - but this is VALID: │ │ Transponder just wasn't reprogrammed after re-registration │ │ │ │ RESOLUTION: │ │ - Record the conflict │ │ - Trust the callsign (pilot knows their registration) │ │ - DON'T assume either is "wrong" │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
Confidence Scoring
Every piece of data has a confidence score:
| Level | Score | Meaning |
|---|---|---|
| VERIFIED | 1.0 | Multiple authoritative sources agree |
| HIGH | 0.8 | Single authoritative source |
| MEDIUM | 0.5 | Derived or inferred from other data |
| LOW | 0.3 | Single non-authoritative source |
| UNVERIFIED | 0.1 | Raw input, not yet validated |
| CONFLICTING | 0.0 | Sources actively disagree |
Examples
- Position from ADS-B with MLAT confirmation: VERIFIED (1.0)
- Position from single receiver, normal signal: HIGH (0.8)
- Position estimated from last known + velocity: MEDIUM (0.5)
- Position from single receiver, weak signal: LOW (0.3)
- Position differs between receivers: CONFLICTING (0.0)
Audit Trail: Never Delete
Rule: Never delete, only supersede.
Record A (original) ├── id: 001 ├── callsign: "UAL123" ├── created_at: 2025-12-28T19:00:00Z ├── supersedes_id: null └── superseded_by_id: 002 ◄─── Points to correction Record B (correction) ├── id: 002 ├── callsign: "UAL124" ◄─── Corrected value ├── created_at: 2025-12-28T19:05:00Z ├── supersedes_id: 001 ◄─── Points to original └── superseded_by_id: null
Query Patterns
-- Current value SELECT * FROM records WHERE superseded_by_id IS NULL; -- History of changes SELECT * FROM records WHERE id = 001 OR supersedes_id = 001 ORDER BY created_at; -- Value at specific time SELECT * FROM records WHERE created_at <= '2025-12-28T19:02:00Z' AND (superseded_at IS NULL OR superseded_at > '2025-12-28T19:02:00Z');
Key Falsehoods Addressed
From FlightAware's Falsehoods article:
Flights
- Flight numbers are NOT unique (reused daily)
- Flights can have MULTIPLE identifiers (codeshares)
- Flight numbers can CHANGE mid-flight
Aircraft
- Registration is NOT permanent (aircraft re-register)
- Mode S address can be STALE (not updated after re-reg)
- "NULL" is a valid callsign (someone set their transponder to it)
Airports
- ICAO codes are NOT permanent (airports move, close)
- Not everything with an ICAO code is an airport (Mars has one: JZRO)
- Railway stations have IATA codes
Positions
- GPS positions can be SPOOFED
- Positions can "teleport" due to errors
- Multiple altitude definitions exist (barometric, geometric, flight level)
Base Classes
@dataclass class TemporalMixin: """All aviation data is temporal.""" valid_from: datetime valid_to: Optional[datetime] = None # None = current @property def is_current(self) -> bool: return self.valid_to is None @dataclass class AuditMixin: """Track provenance of every piece of data.""" id: UUID created_at: datetime source: str supersedes_id: Optional[UUID] = None superseded_by_id: Optional[UUID] = None confidence: float = 0.5
Implementation Order
| Week | Focus |
|---|---|
| 1 | Core entities: RawSBSMessage, MergedSighting, Quarantine |
| 2 | Validation: Callsign, Position, Spoof detection |
| 3 | Temporal model: TemporalMixin, AuditMixin, ConfidenceScore |
| 4 | Identity resolution: Aircraft, Identity, Transponder |
| 5 | Flight model: Flight, FlightIdentifier, FlightLeg |
| 6 | Enrichment: OpenSky integration, Operator resolution |
