ADS-B Entity Model
Table of Contents
1. 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.
2. 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 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
3. 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)
4. 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" │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
5. 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 |
5.1. 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)
6. 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
6.1. 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');
7. Key Falsehoods Addressed
From FlightAware's Falsehoods article:
7.1. Flights
- Flight numbers are NOT unique (reused daily)
- Flights can have MULTIPLE identifiers (codeshares)
- Flight numbers can CHANGE mid-flight
7.2. 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)
7.3. 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
7.4. Positions
- GPS positions can be SPOOFED
- Positions can "teleport" due to errors
- Multiple altitude definitions exist (barometric, geometric, flight level)
8. 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
9. 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 |