Flight Attribution¶
This notebook demonstrates how to fetch flight attributions from the contrails.org Observations API which repackage Google’s L4 flight attributions.
Configuration¶
We define the basic parameters of the call, including an authorization token. This example expects that authorization token to have been stored in the environment variable CONTRAILS_API_KEY.
Contact api@contrails.org if you need an API key.
[1]:
# import dependencies
import json
import os
import requests
import io
import pandas as pd
# import plotly.express as px
import folium
[2]:
# Load API key
# (contact api@contrails.org if you need an API key)
URL = "https://api.contrails.org"
API_KEY = os.environ["CONTRAILS_API_KEY"]
HEADERS = {"x-api-key": API_KEY}
if API_KEY:
print("Key loaded successfully!")
else:
print("Key not found. Check your .env file.")
Key loaded successfully!
Define flight identifier¶
The v1/observations/google/geostationary/L4/attributions endpoint requires a flight_identifier parameter to identify the flight. We demonstrate how to construct this identifier below.
[3]:
# This is the `flight_identifier` format:
# {carrier}~{flight_number}~{departure_date}~{departure_airport_iata}, e.g. AA~6441~2025-05-11~SAF.
# Each part of the flight identifier is required.
CARRIER = "AF" # 2-character IATA airline designator (case-sensitive)
FLIGHT_NUMBER = "6113" # Numeric portion of the flight number only (no carrier prefix)
DEPARTURE_DATE = "2025-02-12" # Date, formatted as 'YYYY-MM-DD'.
DEPARTURE_AIRPORT_IATA = "TLS" # 3-character IATA code (case-sensitive)
flight_identifier = f"{CARRIER}~{FLIGHT_NUMBER}~{DEPARTURE_DATE}~{DEPARTURE_AIRPORT_IATA}"
print(f"Flight identifier: {flight_identifier}")
Flight identifier: AF~6113~2025-02-12~TLS
Fetch flight attribution data¶
Here we make an API request to the L4 attributions endpoint. Note that we wrap the flight_identifier in an array: the endpoint supports lists of flight_identifier values. For more detailed information on the endpoint, see the API documentation.
[4]:
ENDPOINT = "v1/observations/google/geostationary/L4/attributions"
params = {"flight": [flight_identifier]}
r = requests.get(f"{URL}/{ENDPOINT}", params=params, headers=HEADERS)
print(f"HTTP Response Code: {r.status_code} {r.reason}\n")
display(json.loads(r.content))
HTTP Response Code: 200 OK
[{'flight': 'AF~6113~2025-02-12~TLS',
'attributions': [{'start_time': '2025-02-12T09:28:00Z',
'end_time': '2025-02-12T09:41:00Z',
'length_meters': 177895}],
'observations': [{'time': '2025-02-12T09:40:07Z',
'length_meters': 145326,
'source': 'MTG_000_FULL_DISK'},
{'time': '2025-02-12T09:50:07Z',
'length_meters': 132192,
'source': 'MTG_000_FULL_DISK'},
{'time': '2025-02-12T10:10:07Z',
'length_meters': 139347,
'source': 'MTG_000_FULL_DISK'},
{'time': '2025-02-12T10:20:07Z',
'length_meters': 127282,
'source': 'MTG_000_FULL_DISK'}]}]
Understanding flight attributions data¶
Each flight can have a number of attributions and observations. These are listed in the JSON response body under keys attributions and observations respectively.
Attributions¶
Each attribution in attributions represents a distinct segment of the flight in question to which contrail(s) were attributed.
start_timerepresents the beginning of the flight segment to which contrails are attributed.end_timerepresents the end of the flight segment to which contrails are attributed.length_metersrepresents the length, in meters, of the segment(s) of the flight path to which contrails were attributed. This is calculated as the great circle distance between the flight waypoints.
Observations¶
Each observation in observations represents the satellite-based observation of the contrail in question.
timeis when the satellite image was taken in which the contrail was detected (typically the start time of the scan).length_metersis the observed end-to-end length of the linear contrail feature in meters, as detected in the satellite image specified bytime. Note that this length can differ from thelength_metersinattributionsbecause:The contrail may have evolved (e.g., spread, lengthened, etc.) in the atmosphere between formation and observation.
The attribution algorithm was only able to confidently attribute a portion of this contrail to this flight.
sourcerepresents the specific satellite that observed the contrail in question. The google documetation outlines available satellites, and each satellite’s detection bounds.
Example use case with flight attributions data¶
Using data from the attributions endpoint, we visualize which portions of a flight generated contrails using the v1/adsb/telemetry endpoint.
Request Data¶
[5]:
ADSB_ENDPOINT = "v1/adsb/telemetry"
HEADERS = {"accept": "application/vnd.apache.parquet", "x-api-key": API_KEY}
days = ["2025-02-12"] # Can specify multiple days in case of a midnight flight
data = []
for day in days:
for hour in range(24):
params = {"date": f"{day}T{hour:02}"}
r = requests.get(f"{URL}/{ADSB_ENDPOINT}", params=params, headers=HEADERS)
print(f"Loaded data for {day}T{hour:02}")
data.append(pd.read_parquet(io.BytesIO(r.content)))
flight_data = pd.concat(data)
Loaded data for 2025-02-12T00
Loaded data for 2025-02-12T01
Loaded data for 2025-02-12T02
Loaded data for 2025-02-12T03
Loaded data for 2025-02-12T04
Loaded data for 2025-02-12T05
Loaded data for 2025-02-12T06
Loaded data for 2025-02-12T07
Loaded data for 2025-02-12T08
Loaded data for 2025-02-12T09
Loaded data for 2025-02-12T10
Loaded data for 2025-02-12T11
Loaded data for 2025-02-12T12
Loaded data for 2025-02-12T13
Loaded data for 2025-02-12T14
Loaded data for 2025-02-12T15
Loaded data for 2025-02-12T16
Loaded data for 2025-02-12T17
Loaded data for 2025-02-12T18
Loaded data for 2025-02-12T19
Loaded data for 2025-02-12T20
Loaded data for 2025-02-12T21
Loaded data for 2025-02-12T22
Loaded data for 2025-02-12T23
Filter data and plot the target flight¶
[29]:
# Some folium plotting functions for visualizing the flight paths
def add_points_to_map(map_obj, plot_df, correct_dateline_for_plot):
for i, (_, row) in enumerate(plot_df.iterrows()):
lat = row["latitude"]
lon = row["longitude"]
attributed = row["is_attributed"]
if correct_dateline_for_plot:
if lon < 0:
lon += 360
if attributed:
color = "red"
else:
color = "black"
folium.CircleMarker(
location=[lat, lon],
tooltip=f'i={i}, timestamp={row["timestamp"]}, point=({lat},{lon}), attributed={attributed}',
radius=2,
color=color,
).add_to(map_obj)
def map_flight(plot_df1, correct_dateline_for_plot=True, bounds=None):
map_obj = folium.Map()
add_points_to_map(map_obj, plot_df1, correct_dateline_for_plot)
if bounds is None:
bounds = map_obj.get_bounds()
map_obj.fit_bounds(bounds)
return map_obj
[31]:
target_flight = flight_data[(flight_data["flight_number"] == "AF6113")].copy()
# Times are determined from the 'attributions' field from attributions call for AF~6113~2025-02-12~TLS.
target_flight["is_attributed"] = (target_flight["timestamp"] >= "2025-02-12T09:28:00") & (
target_flight["timestamp"] <= "2025-02-12T09:41:00"
)
map_flight(target_flight)
[31]:
[ ]: