Load UKMO data

Support provided for the UK Met Office Unified Model (UM) global deterministic 10 km forecasts. Forecasts are read from the public met-office-atmospheric-model-data S3 bucket.

No credentials are required, but the optional dependencies must be installed with pip install pycontrails[ukmo].

This datalib currently supports air temperature, relative humidity, and specific humidity (derived from the other two) only. It does not provide winds, so the UKMO data cannot be used on its own to run CoCiP. It does provide the fields required by the ISSR, SAC, and PCR models. Fields are available on standard pressure levels only, spaced 25 hPa apart (roughly 1,900 to 3,200 ft) near cruise altitudes. This is coarse compared with model-level data, so thin ice-supersaturated layers are not resolved.

The examples below use forecasts from July 2026. The bucket holds roughly two years of forecasts, so the dates in this notebook will eventually need to be updated.

Forecast availability

The UM global model is initialized at 0Z, 6Z, 12Z, and 18Z. The forecast horizon and timestep depend on the initialization time:

Initialization

Hourly data

3-hourly data

6-hourly data

0Z, 12Z

to +54h

to +144h

to +168h

6Z, 18Z

to +54h

to +69h

none

(Forecasts initialized at 6Z and 18Z before 2026-01-21 18z end at +66h instead of +69h.)

Fields are provided on a regular latitude-longitude grid with 2560 x 1920 points (0.140625 degrees in longitude and 0.09375 degrees in latitude, approximately 10 km at midlatitudes). This datalib does not support regridding.

[1]:
from pycontrails.datalib.ukmo import UM10

Selecting a forecast

By default, all data comes from the single forecast cycle with the shortest lead times that covers the requested time range:

[2]:
um = UM10(
    time=("2026-07-15 12:00", "2026-07-15 13:00"),
    variables=("t", "q"),
    pressure_levels=[300, 250, 200],
)
um
[2]:
UM10
        Timesteps: ['2026-07-15 12', '2026-07-15 13']
        Variables: ['t', 'q']
        Pressure levels: [200, 250, 300]
        Grid: None
[3]:
um.forecast_time
[3]:
datetime.datetime(2026, 7, 15, 12, 0)

A specific forecast cycle can be selected using the forecast_time parameter:

[4]:
um = UM10(
    time=("2026-07-15 12:00", "2026-07-15 13:00"),
    forecast_time="2026-07-15 00:00",
    variables=("t", "q"),
    pressure_levels=[300, 250, 200],
)
um.forecast_time
[4]:
datetime.datetime(2026, 7, 15, 0, 0)

Timesteps are hourly, three-hourly, or six-hourly depending on how far into the forecast the requested time range extends. This week-long time range extends to the end of the 12Z forecast, where only six-hourly data is available:

[5]:
um = UM10(
    time=("2026-07-08 12:00", "2026-07-15 12:00"),
    variables=("t", "q"),
    pressure_levels=[300, 250, 200],
)
print("Forecast time:", um.forecast_time)
print("Timestep:", um.timesteps[1] - um.timesteps[0])
print("Number of timesteps:", len(um.timesteps))
Forecast time: 2026-07-08 12:00:00
Timestep: 6:00:00
Number of timesteps: 29

The time range must be short enough for a single forecast to cover it, otherwise a ValueError is raised. Longer time ranges can be retrieved using the lead_hours parameter, which uses data with a minimum lead time instead of from a single forecast. Each timestep then comes from the most recent forecast initialized at least lead_hours earlier, so consecutive timesteps may come from different forecasts. Because forecasts are initialized every 6 hours, the lead time is within 6 hours of lead_hours (within 12 hours for leads of more than 69 hours, which only the 0Z and 12Z forecasts reach). lead_hours=0 uses the most recent forecast at each timestep. Timesteps are hourly by default for leads up to 48 hours, and six-hourly for longer leads, when the archive no longer publishes every hour. lead_hours cannot be combined with forecast_time.

[6]:
um = UM10(
    time=("2026-07-01 00:00", "2026-07-15 00:00"),
    lead_hours=24,
    variables=("t", "q"),
    pressure_levels=[300, 250, 200],
)
print("Timestep:", um.timesteps[1] - um.timesteps[0])
print("Number of timesteps:", len(um.timesteps))
Timestep: 1:00:00
Number of timesteps: 337

Retrieving data

By default, fields are provided at 300, 275, 250, 225, 200, 175, and 150 hPa (approximately 30,000 to 44,600 ft). Other pressure levels between 1000 hPa and 10 hPa can be requested using the pressure_levels parameter. Only the variables requested are cached, and specific humidity is derived from temperature and relative humidity, so requesting it downloads both fields.

The archive stores each field for the entire globe, so each forecast hour downloads roughly 370 MB for temperature and relative humidity. Processed data is cached locally so repeated requests are much faster.

[7]:
um = UM10(
    time=("2026-07-15 12:00", "2026-07-15 13:00"),
    variables=("t", "q"),
    pressure_levels=[300, 250, 200],
)
mds = um.open_metdataset()
mds
[7]:
MetDataset with data:

<xarray.Dataset> Size: 236MB
Dimensions:                  (longitude: 2560, latitude: 1920, level: 3, time: 2)
Coordinates:
  * longitude                (longitude) float64 20kB -179.9 -179.8 ... 179.9
  * latitude                 (latitude) float64 15kB -89.95 -89.86 ... 89.95
  * level                    (level) float64 24B 200.0 250.0 300.0
    air_pressure             (level) float32 12B 2e+04 2.5e+04 3e+04
    altitude                 (level) float32 12B 1.178e+04 1.036e+04 9.164e+03
  * time                     (time) datetime64[ns] 16B 2026-07-15T12:00:00 20...
    forecast_period          (time) timedelta64[ns] 16B 00:00:00 01:00:00
    forecast_reference_time  datetime64[ns] 8B 2026-07-15T12:00:00
Data variables:
    air_temperature          (longitude, latitude, level, time) float32 118MB dask.array<chunksize=(1280, 960, 1, 1), meta=np.ndarray>
    specific_humidity        (longitude, latitude, level, time) float32 118MB dask.array<chunksize=(2560, 1920, 3, 1), meta=np.ndarray>
Attributes:
    pycontrails_version:  0.64.1.dev28
    provider:             UKMO
    dataset:              UM-10
    product:              forecast

Relative humidity with respect to ice (RHi) at cruise altitude. Values above 1 are ice supersaturated. The UM allows supersaturation, so the plot shows how much it permits:

[8]:
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
from matplotlib.colors import TwoSlopeNorm

from pycontrails.physics import thermo

mds_250 = mds.data.sel(level=250).isel(time=0)
rhi = thermo.rhi(mds_250["specific_humidity"], mds_250["air_temperature"], mds_250["air_pressure"])
ax = rhi.plot(
    x="longitude",
    y="latitude",
    figsize=(10, 4),
    cmap="RdBu",
    norm=TwoSlopeNorm(vcenter=1.0, vmin=0.0, vmax=1.6),
    cbar_kwargs={"label": "relative humidity over ice"},
    subplot_kws=dict(projection=ccrs.PlateCarree()),
    transform=ccrs.PlateCarree(),
).axes
ax.coastlines()
ax.set_title("RHi at 250 hPa, 2026-07-15 12:00");
../_images/notebooks_UKMO_15_0.png

Persistent contrail regions

Air temperature and specific humidity are sufficient to evaluate the Schmidt-Appleman criterion (SAC), ice supersaturated regions (ISSR), and persistent contrail regions (PCR = SAC & ISSR). See the SAC and ISSR notebooks for more on these models.

[9]:
from pycontrails.models.pcr import PCR
from pycontrails.models.sac import SAC

sac = SAC(met=mds).eval()
pcr = PCR(met=mds).eval()
[10]:
fig, axes = plt.subplots(1, 2, figsize=(16, 4), subplot_kw=dict(projection=ccrs.PlateCarree()))

sac["sac"].data.sel(level=250).isel(time=0).plot(
    ax=axes[0],
    x="longitude",
    y="latitude",
    cmap="Reds",
    transform=ccrs.PlateCarree(),
    cbar_kwargs={"label": "SAC satisfied"},
)
axes[0].coastlines()
axes[0].set_title("Schmidt-Appleman criterion at 250 hPa, 2026-07-15 12:00")

pcr["pcr"].data.sel(level=250).isel(time=0).plot(
    ax=axes[1],
    x="longitude",
    y="latitude",
    cmap="Reds",
    transform=ccrs.PlateCarree(),
    cbar_kwargs={"label": "persistent contrail region"},
)
axes[1].coastlines()
axes[1].set_title("Persistent contrail regions at 250 hPa, 2026-07-15 12:00");
../_images/notebooks_UKMO_18_0.png