Get Started¶
Contrails API is a RESTful API that provides contrail modeling and mitigation tools.
This notebook shows basic usage of Contrails API from a Python notebook using the requests package to send HTTP requests.
You can interact with Contrails API with any HTTP compatible client (i.e. curl, Postman).
Authorization¶
Your API key must be provided in
x-api-key
request header.In this notebook, we pluck it from a preset environment variable.
Similarly, we parametrize the URL endpoint as an environment variable. The production API base url for the v0 API is
https://api.contrails.org/v0
.Contact api@contrails.org to request a user account.
[1]:
import os
from pprint import pprint
[2]:
URL = "https://api.contrails.org"
api_key = os.environ["CONTRAILS_API_KEY"]
headers = {"x-api-key": api_key}
Trajectory API (/v0
)¶
Endpoints in the /trajectory/
API each require POST requests. The POST body defines an individual flight: A sequence of discrete temporal spatial waypoints describing the 1D flight trajectory. See the Fleet Computation guide for posting multiple distinct flights in one request.
The request body defines the parameterization of the flight into discrete waypoints. Responses use the same parameterization defined by the request body. For example, if a flight with 85 waypoints is passed into the /trajectory/issr
endpoint, the response object will contain 85 ISSR predictions. These are in one-to-one corresponding with the request body.
We create a synthetic flight to use in this notebook as an example.
Not every field defined in the flight
dictionary below is required for each endpoint. Apart from the four necessary temporal spatial fields (longitude
, latitude
, altitude
, time
), additional aircraft performance variables (engine_efficiency
, aircraft_mass
, …) can be array-like (of the same length as the temporal spatial fields) or scalar-like if a constant value is to be used for all waypoints.
[3]:
import matplotlib.pyplot as plt # pip install matplotlib
import numpy as np # pip install numpy
import pandas as pd # pip install pandas
[4]:
n_waypoints = 100
t0 = "2022-06-07T00:15:00"
t1 = "2022-06-07T02:30:00"
flight = {
"longitude": np.linspace(-29, -50, n_waypoints).tolist(),
"latitude": np.linspace(45, 42, n_waypoints).tolist(),
"altitude": np.linspace(33000, 38000, n_waypoints).tolist(),
"time": pd.date_range(t0, t1, periods=n_waypoints).astype(str).tolist(),
"engine_efficiency": np.random.default_rng(42).uniform(0.2, 0.4, n_waypoints).tolist(),
"aircraft_mass": np.linspace(65000, 62000, n_waypoints).tolist(),
"aircraft_type": "A320",
}
SAC¶
This endpoint calculates Schmidt-Appleman contrail formation criteria (SAC) along a flight trajectory.
The SAC is a binary model that indicates whether the flight forms an initial contrail. In particular, we use the following conventions.
A value of 1 indicates the waypoint satisfies the SAC.
A value of 0 indicates the waypoint does not satisfy the SAC.
A null value indicates that the SAC state is not known for the waypoint. This most often occurs at terminal waypoints when engine efficiency is not known, or when the waypoint is not contained within the domain of the meteorology data.
Engine efficiency is a critical parameter for the SAC. If the engine_efficiency
field is not provided, it calculated via an aircraft performance model from the flight trajectory and the aircraft_type
field.
[5]:
import requests
r = requests.post(f"{URL}/v0/trajectory/sac", json=flight, headers=headers)
print(f"HTTP Response Code: {r.status_code} {r.reason}\n")
r_json = r.json()
for k, v in r_json.items():
print(f"{k}: {v}")
HTTP Response Code: 200 OK
flight_id: 1720721133030796
met_source_provider: ECMWF
met_source_dataset: ERA5
met_source_product: reanalysis
pycontrails_version: 0.52.1
humidity_scaling_name: histogram_matching
humidity_scaling_formula: era5_quantiles -> iagos_quantiles
sac: [1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
[6]:
flight_df = pd.DataFrame(flight).assign(sac=r_json["sac"])
flight_df.plot.scatter(x="longitude", y="latitude", c="sac", cmap="bwr", s=3);
ISSR¶
This endpoint calculates ice super-saturated regions (ISSR) along a flight trajectory. Waypoints for which the ambient atmosphere has relative humidity over ice greater than 100% are considered to be in an ISSR.
We use the same value conventions as with the SAC model.
[7]:
r = requests.post(f"{URL}/v0/trajectory/issr", json=flight, headers=headers)
print(f"HTTP Response Code: {r.status_code} {r.reason}\n")
r_json = r.json()
for k, v in r_json.items():
print(f"{k}: {v}")
HTTP Response Code: 200 OK
flight_id: 1720721137499696
met_source_provider: ECMWF
met_source_dataset: ERA5
met_source_product: reanalysis
pycontrails_version: 0.52.1
humidity_scaling_name: histogram_matching
humidity_scaling_formula: era5_quantiles -> iagos_quantiles
issr: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
[8]:
flight_df["issr"] = r_json["issr"]
flight_df.plot.scatter(x="longitude", y="latitude", c="issr", cmap="bwr", s=1);
CoCiP¶
The /trajectory/cocip
endpoint implements the Contrail Cirrus Prediction (CoCiP) model published in Schumann 2012 and Schumann et al 2012. The API implementation includes updates from Schumann 2015, Teoh 2020, and Teoh
2022.
The CoCiP model requires more meteorology data and compute time than the other models. Consequently, requesting predictions from the /trajectory/cocip
endpoint takes some time.
[9]:
r = requests.post(f"{URL}/v0/trajectory/cocip", json=flight, headers=headers)
print(f"HTTP Response Code: {r.status_code} {r.reason}\n")
r_json = r.json()
# The /trajectory/cocip endpoint includes many fields in the response.
for k, v in r_json.items():
v = str(v)
if len(v) > 80:
v = v[:77] + "..."
print(f"{k}: {v}")
HTTP Response Code: 200 OK
cocip_max_contrail_age: 12 hours
cocip_dt_integration: 10 minutes
flight_id: 1720721142008133
met_source_provider: ECMWF
met_source_dataset: ERA5
met_source_product: reanalysis
pycontrails_version: 0.52.1
nvpm_data_source: ICAO EDB
engine_uid: 01P08CM105
humidity_scaling_name: histogram_matching
humidity_scaling_formula: era5_quantiles -> iagos_quantiles
sac: [1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0...
nox_ei: [0.0076, 0.0076, 0.0076, 0.0076, 0.0076, 0.0076, 0.0076, 0.0076, 0.0076, 0.00...
nvpm_ei_n: [647000000000000.0, 647000000000000.0, 648000000000000.0, 649000000000000.0, ...
energy_forcing: [0.0, 0.0, 680000000000.0, 0.0, 0.0, 790000000000.0, 540000000000.0, 0.0, 0.0...
contrail_age: [0.0, 0.0, 112.0, 0.0, 0.0, 128.0, 127.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ...
initially_persistent: [1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0...
[10]:
# Energy forcing is the primary model output.
ef = np.array(r_json["energy_forcing"], dtype=float)
ef
[10]:
array([0.00e+00, 0.00e+00, 6.80e+11, 0.00e+00, 0.00e+00, 7.90e+11,
5.40e+11, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00,
0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00,
0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 6.10e+11, 5.40e+11,
0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00,
0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00,
0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00,
0.00e+00, 0.00e+00, 2.90e+11, 1.74e+12, 1.32e+12, 1.68e+12,
0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 2.92e+12, 2.54e+12,
1.51e+12, 1.03e+12, 6.30e+11, 1.90e+11, 8.00e+10, 5.00e+10,
5.00e+10, 6.00e+10, 6.00e+10, 7.00e+10, 1.20e+11, 1.50e+11,
1.90e+11, 2.10e+11, 1.90e+11, 1.40e+11, 9.00e+10, 6.00e+10,
2.00e+10, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00,
0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00,
0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00,
0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00,
0.00e+00, 0.00e+00, 0.00e+00, 0.00e+00])
[11]:
flight_df["ef"] = ef
flight_df.plot.scatter(x="longitude", y="latitude", c="ef", cmap="Reds", s=3);
Emissions¶
The /trajectory/emissions
endpoint implements the aircraft performance models described in Teoh 2020 and Teoh 2022.
Presently, the emissions endpoint provides per-waypoint predictions for NOx and nvPM emissions. These values are derived from the ICAO Aircraft Engine Emissions Databank. An engine_uid
field can be supplied to specify the engine type to query the databank. If not provided, the API will assume the engine is the most common engine associated with the aircraft type. Below, we don’t supply the
engine_uid
, and so it is included in the response.
[12]:
r = requests.post(f"{URL}/v0/trajectory/emissions", json=flight, headers=headers)
print(f"HTTP Response Code: {r.status_code} {r.reason}\n")
r_json = r.json()
for k, v in r_json.items():
v = str(v)
if len(v) > 80:
v = v[:77] + "..."
print(f"{k}: {v}")
HTTP Response Code: 200 OK
flight_id: 1720721165741892
met_source_provider: ECMWF
met_source_dataset: ERA5
met_source_product: reanalysis
pycontrails_version: 0.52.1
nvpm_data_source: ICAO EDB
engine_uid: 01P08CM105
humidity_scaling_name: histogram_matching
humidity_scaling_formula: era5_quantiles -> iagos_quantiles
nox_ei: [0.0076, 0.0076, 0.0076, 0.0076, 0.0076, 0.0076, 0.0076, 0.0076, 0.0076, 0.00...
nvpm_ei_n: [647000000000000.0, 647000000000000.0, 648000000000000.0, 649000000000000.0, ...
[13]:
time = pd.to_datetime(flight["time"])
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(time, r_json["nox_ei"], label="nox_ei")
ax2 = ax.twinx()
ax2.plot(time, r_json["nvpm_ei_n"], color="orange", label="nvpm_ei_n")
lines1, labels1 = ax.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax.legend(lines1 + lines2, labels1 + labels2, loc=0)
ax.set_ylabel("NOx emissions index")
ax2.set_ylabel("Nonvolatile particulate matter emissions index number")
ax.set_title("Emissions index for NOx and Nonvolatile Particulate Matter");
Grid API (/v0
)¶
Grid endpoints evaluate a model of interest over a 4D temporal spatial grid. Presently, each grid endpoint requires a single timestamp, and so the grid’s time coordinate contains a single value.
Unlike the trajectory endpoints, each grid endpoint is a GET request requiring query parameters. Commonly used query parameters include:
time
: the timestamp for the grid. Must be an ISO 8601 datetime string or unix timestamp (seconds since epoch).flight_level
: one or multiple flight levels used to downselect the vertical coordinate of the grid. Must be a comma-separated string or list of integers. Available flight levels are 270, 280, 290, 300, 310, 320, 330, 340, 350, 360, 370, 380, 390, 400, 410, 420, 430, 440. If not provided, all flight levels are included in the response.bbox
: a horizontal bounding box for the grid. Must be a comma-separated string or list of length 4. The order of the coordinates is[min_lon, min_lat, max_lon, max_lat]
. The default is the bounding box of the entire domain (most of the entire globe).format
: the format of the response data. This is a case-sensitive string (must be lower-case). Valid choices are:
Examples of netCDF and GeoJSON responses are shown below.
Data Availability¶
Grid endpoints in Contrails API serve data with varying availability. The /v0/grid/availability
endpoint gives a range of times for which each /grid
endpoint serves data.
[ ]:
r = requests.get(f"{URL}/v0/grid/availability", headers=headers)
print(f"HTTP Response Code: {r.status_code} {r.reason}")
pprint(r.json())
SAC, ISSR, PCR¶
GET/v0/grid/sac
GET/v0/grid/pcr
The SAC, ISSR, and PCR grid endpoints all use similar conventions.
We demonstrate the SAC grid endpoint here. This endpoint accepts an optional engine_efficiency
parameter that takes a default value of 0.3.
[14]:
import xarray as xr # pip install xarray
[15]:
time = "2022-06-07T02"
bbox = "-50,0,50,50"
params = {"time": time, "bbox": bbox, "engine_efficiency": 0.32}
r = requests.get(f"{URL}/v0/grid/sac", headers=headers, params=params)
print(f"HTTP Response Code: {r.status_code} {r.reason}")
print(f"Response content-type: {r.headers['content-type']}")
HTTP Response Code: 200 OK
Response content-type: application/netcdf
netCDF Response Format¶
The default response format is netCDF. For the SAC endpoint, the 4D grid holds a single sac
variable. The response can be written out and read with xarray
.
[16]:
with open("sac.nc", "wb") as f:
f.write(r.content)
da = xr.open_dataarray("sac.nc", engine="netcdf4") # pip install netCDF4
da.coords # each netCDF served in the API is a 4D grid
[16]:
Coordinates:
* time (time) datetime64[ns] 2022-06-07T02:00:00
* flight_level (flight_level) int32 270 280 290 300 310 ... 410 420 430 440
* longitude (longitude) float32 -50.0 -49.75 -49.5 ... 49.5 49.75 50.0
* latitude (latitude) float32 0.0 0.25 0.5 0.75 ... 49.25 49.5 49.75 50.0
[17]:
# The grid contains 18 flight levels ranging from 270 to 440.
da.flight_level.values
[17]:
array([270, 280, 290, 300, 310, 320, 330, 340, 350, 360, 370, 380, 390,
400, 410, 420, 430, 440], dtype=int32)
[18]:
# We "squeeze" on time and select a single flight level to plot.
da.squeeze("time").isel(flight_level=10).plot(x="longitude", y="latitude", cmap="bwr");
GeoJSON Response Format¶
Contrails API supports two types of polygon formats: GeoJSON and KML.
We demonstrate the same grid endpoint using the GeoJSON representation here.
See the polygon documentation for additional examples including custom polygon simplification.
[19]:
params["format"] = "geojson"
r = requests.get(f"{URL}/v0/grid/sac", headers=headers, params=params)
print(f"HTTP Response Code: {r.status_code} {r.reason}")
print(f"Response content-type: {r.headers['content-type']}")
r_json = r.json()
HTTP Response Code: 200 OK
Response content-type: application/json
[20]:
import shapely.geometry as sgeom # pip install shapely
[21]:
# The response body is a GeoJSON FeatureCollection. Each feature contains polygons for each flight level.
print(f"GeoJSON type: {r_json['type']}")
# Extract a feature
feature = r_json["features"][8]
pprint(feature["properties"]) # print out the metadata
# Visualize with shapely
# Polygons can have both exterior and interior rings
polygons = sgeom.shape(feature["geometry"])
for poly in polygons.geoms:
plt.plot(*poly.exterior.xy, color="red") # color exterior red
for interior in poly.interiors:
plt.plot(*interior.xy, color="blue") # and interior blue
GeoJSON type: FeatureCollection
{'description': 'Schmidt-Appleman contrail formation criteria',
'engine_efficiency': 0.32,
'humidity_scaling_formula': 'era5_quantiles -> iagos_quantiles',
'humidity_scaling_name': 'histogram_matching',
'level': 350,
'level_long_name': 'Flight Level',
'level_standard_name': 'FL',
'level_units': 'hectofeet',
'met_source_dataset': 'ERA5',
'met_source_product': 'reanalysis',
'met_source_provider': 'ECMWF',
'name': 'sac',
'polygon_iso_value': 0.5,
'pycontrails_version': '0.52.1',
'time': '2022-06-07T02:00:00Z'}
CoCiP¶
The
v0/grid/cocip
endpoint is designed for research purposes. Please use the /v1 Contrail Forecast API to design production use cases (e.g. trials, flight planning, air traffic management) moving forward.
The gridded CoCiP model (Engberg et al 2024) is an abstraction of the original CoCiP model . Instead of working with a single flight trajectory, the gridded version starts with a 4D vector field of trajectory segments each assumed to be in a nominal cruising state. The gridded model then evolves the segments over time using the same rules as the classical model.
Given a fixed trajectory, the original CoCiP model gives a precise prediction of contrail climate forcing resulting from that trajectory. Consequently, the /v0/trajectory/cocip
endpoint should be favored when evaluating contrail forcing of a single flight. On the other hand, the gridded CoCiP model can be used to optimize an unknown trajectory over a 4D grid. The two models widely agree when a flight is in a nominal cruising state.
The /v0/grid/cocip
endpoints requires an aircraft_type
query parameter for CoCiP initialization.
Unlike all other endpoints, the model underpinning the /v0/grid/cocip
endpoint has been precomputed. Only a limited set of 11 aircraft types are currently available.
A320
A20N
A321
A319
A21N
A333
A350
B737
B738
B789
B77W
netCDF Response Format¶
[22]:
params["aircraft_type"] = "A320"
params["format"] = "netcdf"
r = requests.get(f"{URL}/v0/grid/cocip", params=params, headers=headers)
print(f"HTTP Response Code: {r.status_code} {r.reason}")
print(f"Content type: {r.headers['content-type']}")
HTTP Response Code: 200 OK
Content type: application/netcdf
[23]:
with open("cocipgrid.nc", "wb") as f:
f.write(r.content)
ds = xr.open_dataset("cocipgrid.nc", engine="netcdf4")
ds.data_vars # The CoCiP data comes with both energy forcing and contrail age
[23]:
Data variables:
ef_per_m (longitude, latitude, flight_level, time) float32 ...
contrail_age (longitude, latitude, flight_level, time) timedelta64[ns] ...
[24]:
# The energy forcing variable is the primary model output.
da = ds["ef_per_m"]
# Print some metadata present in the netCDF
pprint(ds.attrs | da.attrs)
# We "squeeze" on time and select a single flight level to plot.
da.squeeze("time").isel(flight_level=10).plot(
x="longitude", y="latitude", vmin=-2e8, vmax=2e8, cmap="coolwarm"
);
{'aircraft_type': 'A320',
'cocip_dt_integration': '5 minutes',
'cocip_max_contrail_age': '12 hours',
'humidity_scaling_formula': 'rhi -> (rhi / rhi_adj) ^ rhi_boost_exponent',
'humidity_scaling_name': 'exponential_boost_latitude_customization',
'long_name': 'Energy forcing per meter of flight trajectory',
'met_source_dataset': 'ERA5',
'met_source_product': 'reanalysis',
'met_source_provider': 'ECMWF',
'name': 'cocip',
'pycontrails_version': '0.32.2',
'units': 'J / m'}
GeoJSON Response Format¶
We can specify an energy forcing threshold
parameter when calling the endpoint. The response contains just polygons surrounding grid cells at which the ef_per_m
exceeds the threshold.
In converting from the grid representation to the polygon representation, we exclude degenerate polygons and polygons whose area is doesn’t exceeds some minimal threshold. Excluding these edge cases allows us to focus on regions of high impact. See the polygon documentation for additional polygon simplification examples.
[25]:
params["format"] = "geojson"
params["threshold"] = 2e8
r = requests.get(f"{URL}/v0/grid/cocip", headers=headers, params=params)
print(f"HTTP Response Code: {r.status_code} {r.reason}")
print(f"Response content-type: {r.headers['content-type']}")
r_json = r.json()
HTTP Response Code: 200 OK
Response content-type: application/json
[26]:
# Extract a feature
feature = r_json["features"][10]
pprint(feature["properties"]) # print out the metadata
# Visualize. We see polygons around regions of dark red in the previous plot.
polygons = sgeom.shape(feature["geometry"])
for poly in polygons.geoms:
plt.plot(*poly.exterior.xy, color="red") # color exterior red
for interior in poly.interiors:
plt.plot(*interior.xy, color="blue") # and interior blue
{'aircraft_type': 'A320',
'cocip_dt_integration': '5 minutes',
'cocip_max_contrail_age': '12 hours',
'humidity_scaling_formula': 'rhi -> (rhi / rhi_adj) ^ rhi_boost_exponent',
'humidity_scaling_name': 'exponential_boost_latitude_customization',
'level': 370,
'level_long_name': 'Flight Level',
'level_standard_name': 'FL',
'level_units': 'hectofeet',
'long_name': 'Energy forcing per meter of flight trajectory',
'met_source_dataset': 'ERA5',
'met_source_product': 'reanalysis',
'met_source_provider': 'ECMWF',
'name': 'ef_per_m',
'polygon_iso_value': 200000000.0,
'pycontrails_version': '0.32.2',
'time': '2022-06-07T02:00:00Z',
'units': 'J / m'}
Cleanup
[28]:
os.remove("sac.nc")
os.remove("cocipgrid.nc")
Contrail Forecast API (/v1
)¶
The Contrail Forecast API (/v1
) implements a working specification for contrail forecast data designed for air traffic planners and managers implementing navigational contrail avoidance systems.
The data served from this endpoint contains contrail forcing scaled to a categorical index [0 - 4], with 0 representing no contrail harm and 4 representing the most harmful contrail forming regions.
See Contrail Forecast /v1 notebook for details on accessing forecast data in netCDF and GeoJSON formats.