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 identifierscount | ||
---|---|---|
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 sequenceConvenient 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)
We can also estimate the total fuel consumed:
g.between(takeoff_time, landing_time).emission().fuel_max
3605.03828711759
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
4600.714899922459
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()
.