Contrail Evolution

The /trajectory/cocip-contrail endpoint (added in version 0.8.0) disseminates intermediate contrail evolution data according to the CoCiP model.

Unlike the /trajectory/cocip endpoint, which predicts contrail impact per flight waypoint, the /trajectory/contrail endpoint returns raw contrail predictions, including optical fields. This endpoint could be used for satellite comparison.

The /trajectory/cocip-contrail endpoint supports both single flight and fleet computation. See Fleet Computation for getting started with fleet computation.

This notebook demonstrates basic usage of this endpoint.

[1]:
import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import requests
[2]:
# Define credentials
URL = "https://api.contrails.org"
API_KEY = os.environ["CONTRAILS_API_KEY"]  # put in your API key here
HEADERS = {"x-api-key": API_KEY}
[3]:
# Create synthetic flight

n_waypoints = 200
t0 = "2022-09-21T15:00:00"
t1 = "2022-09-21T19:00:00"

fl = pd.DataFrame()
fl["longitude"] = np.linspace(10, 15, n_waypoints)
fl["latitude"] = np.linspace(35, 65, n_waypoints)
fl["altitude"] = 37000.0
fl["time"] = pd.date_range(t0, t1, periods=n_waypoints).astype(int) // 1_000_000_000

Post a flight to the /trajectory/cocip-contrail endpoint

The response contains a contrail field, which holds all of the intermediate contrail predictions. Like all other /trajectory response models, the contrail data is a list whose items are in one-to-one correspondence with waypoints in the trajectory payload. For example, the 67th element of this contrail list holds contrail evolution data for the 67th waypoint of the flight posted to the endpoint.

[4]:
payload = fl.to_dict("list") | {"aircraft_type": "A321"}

r = requests.post(f"{URL}/v0/trajectory/cocip-contrail", json=payload, headers=HEADERS)
print(f"HTTP Response Code: {r.status_code} {r.reason}")
r_json = r.json()
HTTP Response Code: 200 OK
[5]:
# Convert contrail data in response to a pd.DataFrame

contrail = pd.concat([pd.DataFrame(c) for c in r_json["contrail"]])

# The time field has the same format as the request time.
contrail["time"] = pd.to_datetime(contrail["time"], unit="s")
contrail.head(5)
[5]:
longitude latitude altitude time tau_cirrus tau_contrail rf_sw rf_lw width depth segment_length n_ice_per_m r_ice_vol
0 13.742 57.458 36906.0 2022-09-21 18:00:00 0.0000 0.0546 0.0 0.453 54.0 57.0 0.0 2.830000e+12 3.860000e-07
0 13.858 58.168 36890.0 2022-09-21 18:10:00 0.0008 0.0419 0.0 0.468 179.0 76.0 18495.0 2.578000e+12 5.700000e-07
0 13.888 58.334 36893.0 2022-09-21 18:10:00 0.0006 0.0796 0.0 1.176 146.0 72.0 18361.0 2.657000e+12 7.930000e-07
1 13.866 58.226 36870.0 2022-09-21 18:20:00 0.0020 0.0185 0.0 0.238 511.0 102.0 18856.0 2.495000e+12 6.650000e-07
0 13.917 58.498 36896.0 2022-09-21 18:10:00 0.0000 0.1123 0.0 1.787 108.0 68.0 18248.0 2.729000e+12 8.900000e-07
[6]:
# Visualize the predictions

fig, ax = plt.subplots(figsize=(10, 6))
fl_filt = fl[fl["latitude"].between(55, 63)]
fl_filt.plot(
    x="longitude",
    y="latitude",
    ax=ax,
    lw=1,
    color="red",
    label="flight trajectory",
)

contrail.plot.scatter(
    x="longitude",
    y="latitude",
    c="rf_lw",
    alpha=(10 * contrail["tau_contrail"]).clip(upper=1),
    s=contrail["width"] / 1000,
    ax=ax,
)
ax.set_title("Contrail evolution");
../_images/notebooks_contrail_evolution_7_0.png

Post a fleet to the /trajectory/cocip-contrail endpoint

We visualize predicted the contrail evolution for a synthetic fleet.

The fleet itself is constructed by perturbing the synthetic flight fl.

Warning. The /trajectory/cocip-contrail response are often much larger than the responses on other endpoints. This is exacerbated when posting a fleet.

[7]:
# Construct synthetic fleet with 50 flights

rng = np.random.default_rng(5772156)
fls = []
for fl_id in range(50):
    lon0, lon1, lat0, lat1 = rng.uniform(-5, 5, 4)
    fl_perturb = fl.copy()
    fl_perturb["longitude"] += np.linspace(lon0, lon1, len(fl_perturb))
    fl_perturb["latitude"] += np.linspace(lat0, lat1, len(fl_perturb))
    fl_perturb["flight_id"] = f"{fl_id:2d}"
    fl_perturb["time"] += rng.integers(0, 7200)
    fl_perturb["altitude"] += rng.choice([-2000, 0, 2000])
    fl_perturb["aircraft_type"] = rng.choice(["A320", "B737"])
    fls.append(fl_perturb)
fleet = pd.concat(fls)

# Visualize our synthetic fleet
fleet.plot.scatter("longitude", "latitude", s=0.1, title="Synthetic fleet");
../_images/notebooks_contrail_evolution_9_0.png
[8]:
payload = fleet.to_dict("list")

r = requests.post(f"{URL}/v0/trajectory/cocip-contrail", json=payload, headers=HEADERS)
print(f"HTTP Response Code: {r.status_code} {r.reason}")

r_json = r.json()
contrail = pd.concat([pd.DataFrame(c) for c in r_json["contrail"]])
contrail["time"] = pd.to_datetime(contrail["time"], unit="s")
HTTP Response Code: 200 OK
[9]:
# Visualize the predictions

from matplotlib.animation import FuncAnimation, PillowWriter

fleet = fleet.set_index(pd.to_datetime(fleet["time"], unit="s").dt.floor("1min"))

fig, ax = plt.subplots(figsize=(10, 6))
ax.set_xlim(8, 24)
ax.set_ylim(52, 66)

scat1 = ax.scatter([], [], s=2, color="red")
scat2 = ax.scatter([], [], c=[], vmin=0, vmax=20)

filt = (
    contrail["longitude"].between(8, 24)
    & contrail["latitude"].between(52, 66)
    & contrail["time"].between("2022-09-21T18", "2022-09-22T01")
)
contrail_filt = contrail[filt]
frames = contrail_filt.groupby("time")


def animate(frame):
    print(".", end="")  # progress bar
    time, group = frame
    ax.set_title(time)

    try:
        scat1.set_offsets(fleet.loc[time, ["longitude", "latitude"]])
    except KeyError:
        scat1.set_offsets([[None, None]])

    scat2.set_offsets(group[["longitude", "latitude"]])
    scat2.set_alpha((3 * group["tau_contrail"]).clip(upper=1))
    scat2.set_array(group["rf_lw"])
    scat2.set_sizes(group["width"].clip(upper=20000) / 1000)

    return scat1, scat2


plt.close()
ani = FuncAnimation(fig, animate, frames=frames)
ani.save("evo.gif", dpi=300, writer=PillowWriter(fps=2))

# Show the gif
from IPython.display import Image

with open("evo.gif", "rb") as f:
    display(Image(data=f.read(), format="png"))

# Cleanup
os.remove("evo.gif")
............................................
../_images/notebooks_contrail_evolution_11_1.png
..................
../_images/notebooks_contrail_evolution_11_3.png