Tutorial sessions๏ƒ

I have been running several tutorial sessions where I usually use data recorded from a flight I took to the tutorial venue place in order to illustrate the possibilities of the traffic library. A regular tutorial session is rather interactive, but the demonstration usually goes along showcasing the following possibilities.

I usually record data with jet1090 and traffic can parse the resulting JSONL file.

from traffic.core import Traffic

t = Traffic.from_file("tutorial.jsonl.gz")
t

Traffic

with 6 identifiers
    count
icao24 callsign  
400f99 BAW3AK 1324
4ca216 EIN5GV 45
4863db KLM60B 5
3836ba AIB04TS 1
4d226a RYR40GW 1
753a6a #SXIV## 1

We see several aircraft in the traffic structures, but want to only select the BAW3AK trajectory. In Traffic structures, we can select trajectories based on the icao24 address (the aircraft identifier) or on the callsign. However, only icao24 fields are present in all messages. It is important to reassign a callsign to the selected trajectory in order to propagate its value along all lines.

f = t['400f99'].assign(callsign="BAW3AK")
f

Flight

  • callsign: BAW3AK
  • aircraft: 400f99 ยท ๐Ÿ‡ฌ๐Ÿ‡ง G-DBCJ (A319)
  • start: 2024-06-06 09:23:31.783399820+00:00
  • stop: 2024-06-06 11:17:05.249867916+00:00
  • duration: 0 days 01:53:33.466468096
  • sampling rate: 0 second(s)

A Flight appears with a specific representation in IPython or Jupyter like environments. From that point, we can access several methods. It is often comfortable to start by resampling the trajectory and get proper state vectors:

g = f.resample('1s')
g

Flight

  • callsign: BAW3AK
  • aircraft: 400f99 ยท ๐Ÿ‡ฌ๐Ÿ‡ง G-DBCJ (A319)
  • start: 2024-06-06 09:23:31+00:00
  • stop: 2024-06-06 11:17:05+00:00
  • duration: 0 days 01:53:34
  • sampling rate: 1 second(s)

We can check the underlying pandas DataFrame:

g.data
timestamp frame df icao24 bds NUCp groundspeed track parity lat_cpr ... barometric_setting NACp tcas_operational NICb squawk bds60 bds40 bds50 selected_heading track_unwrapped
0 2024-06-06 09:23:31+00:00 8f400f99381a56699f473ac6439b 17.0 400f99 6.0 7.0 0.0 104.0625 odd 79055.0 ... <NA> <NA> <NA> <NA> <NA> None None None <NA> 104.0625
1 2024-06-06 09:23:32+00:00 a32000b9ff81c30000000001b4a3 20.0 400f99 6.4 7.0 0.0 104.0625 odd 65549.2 ... <NA> <NA> <NA> <NA> <NA> None None None <NA> 104.0625
2 2024-06-06 09:23:33+00:00 a32000b9200815f304b8207cdb8a 20.0 400f99 6.8 7.0 0.0 104.0625 odd 52043.4 ... <NA> <NA> <NA> <NA> <NA> None None None <NA> 104.0625
3 2024-06-06 09:23:34+00:00 a32000b9200815f304b8207cdb8a 19.0 400f99 7.2 7.0 0.0 104.0625 odd 38537.6 ... <NA> <NA> <NA> <NA> <NA> None None None <NA> 104.0625
4 2024-06-06 09:23:35+00:00 a32000b9200815f304b8207cdb8a 18.0 400f99 7.6 7.0 0.0 104.0625 odd 25031.8 ... <NA> <NA> <NA> <NA> <NA> None None None <NA> 104.0625
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
6810 2024-06-06 11:17:01+00:00 8c400f99401e06f8c99c7a567858 17.0 400f99 6.0 6.0 0.0 270.0000 odd 97380.0 ... 1016.8 9.0 0.0 0.0 2312.0 {'bds': '60', 'heading': 271.0546875, 'IAS': 1... {'bds': '40', 'selected_mcp': 3000, 'barometri... {'bds': '50', 'roll': 0.3515625, 'track': 269.... 75.234375 270.0000
6811 2024-06-06 11:17:02+00:00 8c400f99401e01426d99b7c3a9e5 17.0 400f99 6.0 6.0 0.0 270.0000 even 41270.0 ... 1016.8 9.0 0.0 0.0 2312.0 {'bds': '60', 'heading': 271.0546875, 'IAS': 1... {'bds': '40', 'selected_mcp': 3000, 'barometri... {'bds': '50', 'roll': 0.3515625, 'track': 269.... 75.234375 270.0000
6812 2024-06-06 11:17:03+00:00 8c400f99401e06f8c99c7a567858 17.0 400f99 6.0 6.0 0.0 270.0000 odd 97380.0 ... 1016.8 9.0 0.0 0.0 2312.0 {'bds': '60', 'heading': 271.0546875, 'IAS': 1... {'bds': '40', 'selected_mcp': 3000, 'barometri... {'bds': '50', 'roll': 0.3515625, 'track': 269.... 75.234375 270.0000
6813 2024-06-06 11:17:04+00:00 a900122c200815f304b820c3a051 21.0 400f99 6.0 6.0 0.0 270.0000 odd 97380.0 ... 1016.8 9.0 0.0 0.0 2312.0 {'bds': '60', 'heading': 271.0546875, 'IAS': 1... {'bds': '40', 'selected_mcp': 3000, 'barometri... {'bds': '50', 'roll': 0.3515625, 'track': 269.... 75.234375 270.0000
6814 2024-06-06 11:17:05+00:00 8c400f99401e06f8c99c7a567858 17.0 400f99 6.0 6.0 0.0 270.0000 odd 97380.0 ... 1016.8 9.0 0.0 0.0 2312.0 {'bds': '60', 'heading': 271.0546875, 'IAS': 1... {'bds': '40', 'selected_mcp': 3000, 'barometri... {'bds': '50', 'roll': 0.3515625, 'track': 269.... 75.234375 270.0000

6815 rows ร— 33 columns

g.start
Timestamp('2024-06-06 09:23:31+0000', tz='UTC')
g.callsign
'BAW3AK'
g.duration
Timedelta('0 days 01:53:34')
g.first('10 min')

Flight

  • callsign: BAW3AK
  • aircraft: 400f99 ยท ๐Ÿ‡ฌ๐Ÿ‡ง G-DBCJ (A319)
  • start: 2024-06-06 09:23:31+00:00
  • stop: 2024-06-06 09:33:30+00:00
  • duration: 0 days 00:09:59
  • sampling rate: 1 second(s)

A convenient way to explore trajectories is to have them displayed in a Leaflet widget.

g.first('10 min').map_leaflet(zoom=14)

A common use case for on ground trajectories is to identify on which parking positions an aircraft is parked. For that purpose, traffic uses data from the OpenStreetMap database.

from traffic.data import airports

airports['LFBO']

Airport

Toulouse-Blagnac Airport (France) LFBO/TLS
(43.629101, 1.36382), altitude: 499

Representations are available for matplotlib, altair or more frameworks. The altair representation is accessible and configurable here:

airports['LFBO'].geoencode()

Any attribute that is a possible value for the aeroway key on OpenStreetMap can be accessed on the airport structure, resulting in a GeoDataFrame that we further use for our analysis.

airports['LFBO'].parking_position
id_ type_ latitude longitude geometry aeroway ref
288 365842702 way 43.630848 1.370834 LINESTRING (1.3705 43.63029, 1.37054 43.63044,... parking_position U12
289 365842703 way 43.630851 1.371080 LINESTRING (1.37068 43.63026, 1.37072 43.63043... parking_position U11
290 365842704 way 43.630786 1.371459 LINESTRING (1.37116 43.63019, 1.3713 43.6304, ... parking_position U10
291 365842706 way 43.630545 1.371833 LINESTRING (1.3713 43.6304, 1.37145 43.63048, ... parking_position E62
292 365842707 way 43.630192 1.372145 LINESTRING (1.37158 43.62989, 1.37271 43.6305) parking_position E60
... ... ... ... ... ... ... ...
573 1182203291 way 43.623641 1.378046 LINESTRING (1.37831 43.62338, 1.37778 43.6239) parking_position B24
574 1182218552 way 43.623405 1.378799 LINESTRING (1.37899 43.62323, 1.37861 43.62358) parking_position A25
575 1182218553 way 43.624261 1.377523 LINESTRING (1.37719 43.62458, 1.37786 43.62394) parking_position B41
576 1182218554 way 43.623935 1.376916 LINESTRING (1.37726 43.62361, 1.37658 43.62426) parking_position B31
577 1182218555 way 43.623630 1.376283 LINESTRING (1.37592 43.62398, 1.37665 43.62328) parking_position B21

125 rows ร— 7 columns

Next, the method on_parking_position() selects all segments that intersect any parking positions on the airport. Since a trajectory can intersect a geometry several times, the resulting structure is a FlightIterator. If we want a Flight, we need to select one of the segments in the iterator:

g.first('10 min').on_parking_position('LFBO')

FlightIterator

Flight

  • callsign: BAW3AK
  • aircraft: 400f99 ยท ๐Ÿ‡ฌ๐Ÿ‡ง G-DBCJ (A319)
  • start: 2024-06-06 09:23:45+00:00
  • stop: 2024-06-06 09:28:55+00:00
  • duration: 0 days 00:05:10
  • sampling rate: 1 second(s)
# Meaning: the longest option in duration
g.first('10 min').on_parking_position('LFBO').max('duration')

Flight

  • callsign: BAW3AK
  • aircraft: 400f99 ยท ๐Ÿ‡ฌ๐Ÿ‡ง G-DBCJ (A319)
  • start: 2024-06-06 09:23:45+00:00
  • stop: 2024-06-06 09:28:55+00:00
  • duration: 0 days 00:05:10
  • sampling rate: 1 second(s)

This method is used in the implementation of the pushback() detection method.

g.first('10 min').pushback('LFBO')

Flight

  • callsign: BAW3AK
  • aircraft: 400f99 ยท ๐Ÿ‡ฌ๐Ÿ‡ง G-DBCJ (A319)
  • start: 2024-06-06 09:24:27+00:00
  • stop: 2024-06-06 09:26:27+00:00
  • duration: 0 days 00:02:00
  • sampling rate: 1 second(s)

We can highlight this part in the Leaflet widget:

g.first('10 min').map_leaflet(
    zoom=16,
    highlight={"red": 'pushback("LFBO")'},
)

We can also look at the last part of the trajectory:

g.last('30 min').map_leaflet(zoom=10)

There is a holding_pattern() detection method included, but here the shape of the trajectory is not consistent with the usual horse racetrack shape and is not labelled as is:

g.holding_pattern()

FlightIterator

Empty sequence

Convenient methods include the detection of landing runways (ILS means โ€œInstrument Landing Systemโ€, i.e., the system that guides aircraft on autopilot to align perfectly with a runway). Check the documentation for the aligned_on_ils() method.

g.aligned_on_ils('EGLL')

FlightIterator

Flight

  • callsign: BAW3AK
  • aircraft: 400f99 ยท ๐Ÿ‡ฌ๐Ÿ‡ง G-DBCJ (A319)
  • start: 2024-06-06 11:03:13+00:00
  • stop: 2024-06-06 11:07:43+00:00
  • duration: 0 days 00:04:30
  • sampling rate: 1 second(s)

The next() method selects the first segment in the list. We can then detect the actual landing time, or more precisely, use the time at runway threshold as a proxy for the actual landing time.

g.aligned_on_ils('EGLL').next().stop
Timestamp('2024-06-06 11:07:43+0000', tz='UTC')

This can also be rewritten for legibility as:

g.next("aligned_on_ils('EGLL')").stop
Timestamp('2024-06-06 11:07:43+0000', tz='UTC')

The tentative holding pattern here in London Heathrow Airport is labelled as OCK (stands for โ€œOckhamโ€, colocated with the OCK VOR). VOR and other navaids are listed in part in the data part of the library.

from traffic.data import navaids

m = g.last('30 min').map_leaflet(
    zoom=10,
    highlight={
        "red": 'aligned_on_ils("EGLL")',
        "#f58518": 'aligned_on_navpoint("OCK")',
    },
)
m.add(navaids['OCK'])
m

Other parts of the library focus on fuel estimation provided by OpenAP. The library is fully integrated in traffic, and can be used to estimate flight phases and emissions.

# resample first to limit the size of the javascript
g.phases().resample('10s').chart('phase').encode(y="phase", color='phase').mark_point()

We can also estimate the fuel flow (and plot with Matplotlib):

import matplotlib.pyplot as plt

fig, ax = plt.subplots(2, 1, sharex=True)
g.plot_time(ax[0], y='altitude')

takeoff_time = g.next("takeoff_from_runway('LFBO')").stop
landing_time = g.next("aligned_on_ils('EGLL')").stop
g.between(takeoff_time, landing_time).emission().plot_time(ax[1], y='fuelflow')

for ax_ in ax:
    ax_.spines['right'].set_visible(False)
    ax_.spines['top'].set_visible(False)
_images/tutorial_23_0.png

We can also estimate the total fuel consumed:

g.between(takeoff_time, landing_time).emission().fuel_max
3983.308914300498

Warning

Note that it is not reasonable to consider these models on the ground and that it can result to big discrepancies.

g.emission().fuel_max
5009.328479715914

Hint

For any column in the pandas DataFrame wrapped by a Flight structure, traffic provides attributes to aggregate all values in the column. g.fuel_max is equivalent to g.data.fuel.max().

Advanced topics๏ƒ