Vertical Profile#

This document shows how to use the /trajectory/profile endpoint (added in version 0.12.0) to visualize CoCiP over a vertical profile of a trajectory.

The predictions provided in the response contain elements of both the CoCiP trajectory model and the Gridded CoCiP model. In particular, this endpoint computes a 2D grid of predictions parameterized by the altitude and time of the trajectory. The longitude, latitude, and azimuth used to construct this grid are taken from the trajectory itself. Aircraft performance variables are assumed to be nominal (the aircraft is always assumed to be in a cruise state for the gridded model). There are notable differences between the trajectory and grid models that are important to understand.

[1]:
import os

import requests  # pip install requests
import numpy as np  # pip install numpy
import pandas as pd  # pip install pandas
import matplotlib.pyplot as plt  # pip install matplotlib
[2]:
# Define credentials
URL = os.environ["API_URL_BASE"]  # https://api.contrails.org/v0
api_key = os.environ["CONTRAILS_API_KEY"]  # put in your API key here
headers = {"x-api-key": api_key}

Create a synthetic flight trajectory#

The cell below follows the same recipe used in Get Started and Customizing Requests for creating a synthetic flight. In particular, the flight below is not meant to be realistic; it only serves as an example. The trajectory itself was cherry-picked to fly through regions of potential contrail formation according to CoCiP.

[3]:
n_waypoints = 300

t0 = "2023-01-16T09:00:00"
t1 = "2023-01-16T14:00:00"
time = pd.date_range(t0, t1, periods=n_waypoints)

# fmt: off
altitude = np.r_[
    np.linspace(10000, 30000, 30),   # Climb to 30000 ft
    [30000 for _ in range(40)],      # Cruise at 30000 ft
    [31000, 32000],                  # Climb to 32000 ft
    [32000 for _ in range(50)],      # Cruise at 32000 ft
    [33000, 34000, 35000, 36000],    # Climb to 36000 ft
    [36000 for _ in range(60)],      # Cruise at 36000 ft
    [37000, 38000, 39000, 40000],    # Climb to 40000 ft
    [40000 for _ in range(80)],      # Cruise at 40000 ft
    np.linspace(40000, 10000, 30),   # Descend to 10000 ft
]
# fmt: on

flight = {
    "longitude": np.linspace(-10, -60, n_waypoints).tolist(),
    "latitude": np.linspace(45, 40, n_waypoints).tolist(),
    "altitude": altitude.tolist(),
    "time": time.astype(int).tolist(),
    "aircraft_type": "A320",
}

Make the request#

Expect requests to the /trajectory/profile endpoint to take 1 - 2 minutes to complete. For faster but less precise predictions, set mode=grid on the request body to approximate CoCiP predictions using a pre-computed gridded model (the same data served in /grid/cocip). This “grid” mode of computation is not available for all trajectories and is not demonstrated in this notebook.

[4]:
endpoint = f"{URL}/trajectory/profile"

r = requests.post(endpoint, json=flight, headers=headers)
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/json

Working with the response#

The profile field contains the predictions for the vertical profile of the trajectory.

The profile predictions are expressed in energy forcing per meter of flight trajectory [J / m]. The underlying model assumes the aircraft is in a nominal state. See the comparison document, which discusses the differences between the trajectory and grid models. Here, the profile predictions behave like the grid model.

Extract as numpy arrays#

The JSON response can easily be converted to numpy arrays.

[5]:
r_json = r.json()

profile = r_json["profile"]
profile_fls = np.array([p["flight_level"] for p in profile])
ef_arr = np.array([p["ef_per_m"] for p in profile])

Converting to pandas or xarray#

The data can also be converted to pandas or xarray data structures.

[6]:
df = pd.DataFrame(ef_arr.T, index=time, columns=profile_fls)
df = df.rename_axis("time", axis=0).rename_axis("flight_level", axis=1)
df.head()
[6]:
flight_level 270 280 290 300 310 320 330 340 350 360 370 380 390 400 410 420 430 440
time
2023-01-16 09:00:00.000000000 0.0 0.0 0.0 0.0 182300000.0 101400000.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
2023-01-16 09:01:00.200668896 0.0 0.0 0.0 0.0 177900000.0 110900000.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
2023-01-16 09:02:00.401337792 0.0 0.0 0.0 0.0 166100000.0 105500000.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
2023-01-16 09:03:00.602006688 0.0 0.0 0.0 0.0 184100000.0 99800000.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
2023-01-16 09:04:00.802675585 0.0 0.0 0.0 0.0 182600000.0 118500000.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
[7]:
da = df.unstack().to_xarray()

# Fill in some metadata (this is optional)
da.attrs = {"long_name": "Energy forcing per meter of flight trajectory", "units": "J / m"}
da["flight_level"].attrs = {"long_name": "Flight level", "units": "hectofeet"}
da["time"].attrs = {"long_name": "Time", "units": "UTC"}

print(da)
<xarray.DataArray (flight_level: 18, time: 300)>
array([[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.]])
Coordinates:
  * flight_level  (flight_level) int64 270 280 290 300 310 ... 410 420 430 440
  * time          (time) datetime64[ns] 2023-01-16T09:00:00 ... 2023-01-16T14...
Attributes:
    long_name:  Energy forcing per meter of flight trajectory
    units:      J / m

Visualize#

Visualize profile predictions#

We use xarray as a convenience to visualize the data.

[8]:
da.plot(vmin=-1e9, vmax=1e9, cmap="coolwarm");
../_images/notebooks_vertical_profile_15_0.png

View the trajectory within the profile grid#

[9]:
da["altitude"] = da["flight_level"] * 100.0
da = da.swap_dims(flight_level="altitude")
da["altitude"].attrs = {"long_name": "Altitude", "units": "feet"}
[10]:
fig, ax = plt.subplots()
da.plot(vmin=-1e9, vmax=1e9, cmap="coolwarm", ax=ax)

ax.set_ylim(8000, 45000)
x0, x1 = ax.get_xlim()
ax.set_xlim(x0 - 0.03 * (x1 - x0), x1 + 0.03 * (x1 - x0))

ax.plot(time, altitude, color="black", lw=1);
../_images/notebooks_vertical_profile_18_0.png

Request the plot directly through the API#

By including a format="png" key-value pair in the request body, the API will return a static plot as a PNG image. (The API backend implementation is identical to what is used in this notebook.) You won’t be able to customize this plot, but it is a convenient way to see the predictions without writing any code.

[11]:
r = requests.post(endpoint, json=flight | {"format": "png"}, headers=headers)
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: image/png
[12]:
# Display in the notebook
from IPython.display import Image

display(Image(data=r.content, format="png"))
../_images/notebooks_vertical_profile_21_0.png