Working with Model Level Met

Meteorology providers may choose to compute their data on different vertical levels. For example, the ECMWF IFS product is computed on 137 model levels. The pressure of a given model level is not constant, but varies with time and horizontal position. The pressure at a given point can be computed following ECMWF guidelines.

All pycontrails models (e.g. Cocip, ISSR, SAC) require that the vertical coordinate of meteorological data is provided on pressure levels (hPa). This notebook converts model level data to pressure level data using the ERA5ModelLevel class. (Previously, the metview library was used for this purpose, but as of pycontrails v0.54.0 this re-interpolation onto pressure levels is done in-house.)

[1]:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from pycontrails import Fleet, Flight
from pycontrails.datalib.ecmwf import ERA5, ERA5ModelLevel
from pycontrails.models.cocip import Cocip
from pycontrails.models.humidity_scaling import HistogramMatching
from pycontrails.models.ps_model import PSFlight

Target pressure levels

We must specify both model levels and target pressure levels. The model levels are the levels at which the meteorological data is provided. The target pressure levels are the levels at which we want to interpolate the data. The target pressure levels are chosen based on the altitudes at which contrails form.

Assuming a constant surface pressure of 1013.25 hPa, the table below shows that ECMWF model levels occur roughly every 10 hPa at altitudes for which contrails form. This informs how we set our target pressure levels: We define our target pressure levels ranging from 170 hPa to 390 hPa, spaced every 10 hPa. A finer spacing than this may be redundant (pycontrails models perform linear interpolation between pressure levels), and a coarser spacing would result in loss of information.

[2]:
url = "https://confluence.ecmwf.int/display/UDOC/L137+model+level+definitions"
df = pd.read_html(url, na_values="-", index_col="n")[0].rename_axis("model_level")
df.loc[70:90]  # model levels 70 - 90 agree with our ERA5ModelLevel object below
[2]:
a [Pa] b ph [hPa] pf [hPa] Geopotential Altitude [m] Geometric Altitude [m] Temperature [K] Density [kg/m^3]
model_level
70 15508.256836 0.011806 167.0450 163.0927 13077.79 13104.70 216.65 0.262244
71 16026.115234 0.014816 175.2731 171.1591 12771.64 12797.30 216.65 0.275215
72 16527.322266 0.018318 183.8344 179.5537 12467.99 12492.44 216.65 0.288713
73 17008.789063 0.022355 192.7389 188.2867 12166.81 12190.10 216.65 0.302755
74 17467.613281 0.026964 201.9969 197.3679 11868.08 11890.24 216.65 0.317357
75 17901.621094 0.032176 211.6186 206.8078 11571.79 11592.86 216.65 0.332536
76 18308.433594 0.038026 221.6146 216.6166 11277.92 11297.93 216.65 0.348308
77 18685.718750 0.044548 231.9954 226.8050 10986.70 11005.69 216.74 0.364545
78 19031.289063 0.051773 242.7719 237.3837 10696.22 10714.22 218.62 0.378253
79 19343.511719 0.059728 253.9549 248.3634 10405.61 10422.64 220.51 0.392358
80 19620.042969 0.068448 265.5556 259.7553 10114.89 10130.98 222.40 0.406868
81 19859.390625 0.077958 277.5852 271.5704 9824.08 9839.26 224.29 0.421790
82 20059.931641 0.088286 290.0548 283.8200 9533.20 9547.49 226.18 0.437130
83 20219.664063 0.099462 302.9762 296.5155 9242.26 9255.70 228.08 0.452897
84 20337.863281 0.111505 316.3607 309.6684 8951.30 8963.90 229.97 0.469097
85 20412.308594 0.124448 330.2202 323.2904 8660.32 8672.11 231.86 0.485737
86 20442.078125 0.138313 344.5663 337.3932 8369.35 8380.36 233.75 0.502825
87 20425.718750 0.153125 359.4111 351.9887 8078.41 8088.67 235.64 0.520367
88 20361.816406 0.168910 374.7666 367.0889 7787.51 7797.04 237.53 0.538370
89 20249.511719 0.185689 390.6450 382.7058 7496.68 7505.51 239.42 0.556842
90 20087.085938 0.203491 407.0583 398.8516 7205.93 7214.09 241.31 0.575790

Make the CDS model level request

[3]:
time = ("2024-01-15T00", "2024-01-15T18")

era5ml = ERA5ModelLevel(
    time=time,
    variables=("t", "q", "u", "v", "w", "ciwc"),
    grid=1,  # horizontal resolution, 0.25 by default
    model_levels=range(70, 91),
    pressure_levels=np.arange(170, 400, 10),
)
met = era5ml.open_metdataset()


era5sl = ERA5(
    time=time,
    variables=("tsr", "ttr"),
    grid=1,
    pressure_levels=-1,
)
rad = era5sl.open_metdataset()

Missing values

Converting from model levels to pressure levels may result in some missing values. These are plotted below.

Points at which the surface pressure is significantly lower than the 1013.25 hPa reference surface pressure will result in missing values at the lowest-altitude target pressure levels. Additional model levels can be downloaded to decrease the number of missing values. Missing values do not occur at higher-altitude target pressure levels.

[4]:
da = met.data["air_temperature"]
da.isnull().groupby("level").mean(...).plot()
plt.ylabel("Proportion of missing values")
plt.title("Missing values by pressure level");
../_images/notebooks_model-levels_7_0.png
[5]:
# There are no missing values above 310 hPa
tmp = da.isnull().isel(time=0).sel(level=slice(310, None))
tmp.plot(x="longitude", y="latitude", add_colorbar=False, col="level", col_wrap=3)
plt.gcf().suptitle("Position of missing values by pressure level", y=1.02);
../_images/notebooks_model-levels_8_0.png

Download ADS-B

For flight trajectories, we use a sample from Contrails API. We artificially shift the times of the flight trajectories to overlap the times of the ERA5 data.

[6]:
df = pd.read_csv("https://apidocs.contrails.org/_static/fleet_sample.csv", parse_dates=["time"])
df["time"] = df["time"].dt.tz_convert(None)
df = df.rename(columns={"altitude": "altitude_ft"})

# Artificially shift time to the date of interest
df["time"] = df["time"] + (pd.Timestamp("2024-01-15") - df["time"].min())

# Convert to a pycontrails Fleet instance, keeping only aircraft type covered by the PS model
ps_flight = PSFlight()
flights = []
for flight_id, group in df.groupby("flight_id"):
    aircraft_type = group["aircraft_type"].iloc[0]
    if not ps_flight.check_aircraft_type_availability(aircraft_type, raise_error=False):
        continue

    engine_uid = group["engine_uid"].iloc[0]
    group = group.drop(columns=["aircraft_type", "engine_uid", "flight_id"])
    flight = Flight(group, aircraft_type=aircraft_type, engine_uid=engine_uid, flight_id=flight_id)
    flights.append(flight)

fleet = Fleet.from_seq(flights)
[7]:
ax = plt.subplot()
for flight in flights:
    flight.plot(ax=ax, alpha=0.3)
../_images/notebooks_model-levels_11_0.png

Run CoCiP

We run CoCiP on the model-level ERA5 data.

[8]:
cocip = Cocip(
    met=met,
    rad=rad,
    dt_integration="5 min",
    max_age="12 hours",
    aircraft_performance=PSFlight(),
    humidity_scaling=HistogramMatching(),
)
cocip_pred = cocip.eval(fleet)
contrail = cocip.contrail

Visualize output

[9]:
ax = plt.subplot()
for flight in flights:
    flight.plot(ax=ax, color="gray", alpha=0.05)

contrail.plot.scatter(
    x="longitude",
    y="latitude",
    s=contrail["width"] / 100000,
    c=contrail["tau_contrail"],
    vmin=0,
    vmax=0.3,
    alpha=0.05,
    zorder=2,
    ax=ax,
);
../_images/notebooks_model-levels_15_0.png