ARPEGE (experimental)¶
METEO FRANCE provides forecasts from a modified version of the ARPEGE global NWP model that allows supersaturation with respect to ice. This experimental model was developed in the frame of the CICONIA SESAR EJU project and validated against IAGOS observations (see Arriolabengoa et al. 2025).
Global forecasts are initialized at 0z and 12z and provide hourly data out to 48 hours on pressure levels ranging from 1000hPa to 100hPa with an horizontal resolution of 0.25°. Data are freely distributed for research purposes only, through the CC BY-NC-SA license. Credentials for FTP access will be given on e-mail request to req_cred.contrails_arpege-cnrm@meteo.fr. (Please describe your organization and related research activities in the email body.)
Due to its experimental status, the forecast FTP server is not monitored 24/7 and may experience interruptions or be suspended.
This notebook provides instructions for
accessing experimental ARPEGE forecasts through the METEO FRANCE FTP server, and
preprocessing experimental ARPEGE forecasts for use in pycontrails.
Forecast access¶
The METEO FRANCE FTP server provides access to recent forecasts (typically those initialized within the last 48 hours) but does not provide a long-term forecast archive.
Contrails.org maintains a limited historical archive of experimental ARPEGE forecasts that can be shared with permission from METEO FRANCE. To request historical ARPEGE data from Contrails.org, email all@contrails.org and copy req_cred.contrails_arpege-cnrm@meteo.fr.
[1]:
import os
from ftplib import FTP
[2]:
ftp = FTP("ftp.umr-cnrm.fr")
ftp.login(os.environ["FTP_USER"], os.environ["FTP_PASSWORD"])
ftp.cwd("arpege/glob025")
ftp.nlst()
[2]:
['20260720T0000P',
'20260904T0000P',
'20260906T1500P',
'20260908T1200P',
'20260909T0000P',
'20260909T1200P',
'20260910T0000P']
Data are provided in one GRIB file per forecast cycle. GRIB files contain one message per variable, per level, and per time step, and are about 7 GB in size.
[3]:
sink = "tmp.grib"
with open(sink, "wb") as f:
ftp.retrbinary("RETR 20260910T0000P/grid.arpege-forecast.glob025.grib", f.write)
Pressure-level variables are provided with a variable vertical resolution (25 hPa from 100-300 hPa and 900-1000 hPa, and 50 hPa elsewhere) and hourly time steps from 1 to 48 hours. Nine parameters are available:
Parameter |
Description |
Units |
Notes |
|---|---|---|---|
|
Zonal (eastward) wind |
m s\(^{-1}\) |
|
|
Meridional (northward) wind |
m s\(^{-1}\) |
|
|
Temperature |
K |
|
|
Cloud ice water content |
kg kg\(^{-1}\) |
|
|
Cloud fraction |
1 |
|
|
Geopotential |
m\(^2\) s\(^{-2}\) |
|
|
Relative humidity |
% |
with respect to ice |
|
Potential vorticity |
K m\(^2\) kg\(^{-1}\) s\(^{-1}\) |
|
|
Vertical velocity |
Pa s\(^{-1}\) |
pressure coordinates, positive downward |
Pressure-level variables can be read as an xarray Dataset by filtering by vertical coordinate:
[4]:
import xarray as xr
[5]:
pl = xr.open_dataset(
sink,
engine="cfgrib",
chunks={"step": 1},
backend_kwargs={"filter_by_keys": {"typeOfLevel": "isobaricInhPa"}},
)
pl
[5]:
<xarray.Dataset> Size: 43GB
Dimensions: (step: 48, isobaricInhPa: 24, latitude: 721, longitude: 1440)
Coordinates:
* step (step) timedelta64[ns] 384B 01:00:00 ... 2 days 00:00:00
valid_time (step) datetime64[ns] 384B dask.array<chunksize=(1,), meta=np.ndarray>
* isobaricInhPa (isobaricInhPa) float64 192B 1e+03 950.0 ... 125.0 100.0
* latitude (latitude) float64 6kB 90.0 89.75 89.5 ... -89.5 -89.75 -90.0
* longitude (longitude) float64 12kB 0.0 0.25 0.5 ... 359.2 359.5 359.8
time datetime64[ns] 8B ...
Data variables:
u (step, isobaricInhPa, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
v (step, isobaricInhPa, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
t (step, isobaricInhPa, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
ciwc (step, isobaricInhPa, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
cc (step, isobaricInhPa, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
z (step, isobaricInhPa, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
r (step, isobaricInhPa, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
pv (step, isobaricInhPa, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
w (step, isobaricInhPa, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
Attributes:
GRIB_edition: 2
GRIB_centre: lfpw
GRIB_centreDescription: French Weather Service - Toulouse
GRIB_subCentre: 0
Conventions: CF-1.7
institution: French Weather Service - Toulouse
history: 2026-09-10T16:32 GRIB to CDM+CF via cfgrib-0.9.1...GRIB files also include three single-level variables:
Parameter |
Description |
Units |
Notes |
|---|---|---|---|
|
Surface downwelling shortwave flux |
J m\(^{-2}\) |
Accumulated |
|
TOA net shortwave flux |
J m\(^{-2}\) |
Positive downward, accumulated |
|
TOA net longwave flux |
J m\(^{-2}\) |
Positive downward, accumulated |
[6]:
sl = xr.open_dataset(
sink,
engine="cfgrib",
chunks={"step": 1},
backend_kwargs={"filter_by_keys": {"typeOfLevel": ["nominalTop", "surface"]}},
)
sl
[6]:
<xarray.Dataset> Size: 586MB
Dimensions: (step: 47, latitude: 721, longitude: 1440)
Coordinates:
* step (step) timedelta64[ns] 376B 01:00:00 ... 1 days 23:00:00
valid_time (step) datetime64[ns] 376B dask.array<chunksize=(1,), meta=np.ndarray>
* latitude (latitude) float64 6kB 90.0 89.75 89.5 ... -89.5 -89.75 -90.0
* longitude (longitude) float64 12kB 0.0 0.25 0.5 0.75 ... 359.2 359.5 359.8
time datetime64[ns] 8B ...
surface float64 8B ...
nominalTop float64 8B ...
Data variables:
ssrd (step, latitude, longitude) float32 195MB dask.array<chunksize=(1, 721, 1440), meta=np.ndarray>
tsr (step, latitude, longitude) float32 195MB dask.array<chunksize=(1, 721, 1440), meta=np.ndarray>
ttr (step, latitude, longitude) float32 195MB dask.array<chunksize=(1, 721, 1440), meta=np.ndarray>
Attributes:
GRIB_edition: 2
GRIB_centre: lfpw
GRIB_centreDescription: French Weather Service - Toulouse
GRIB_subCentre: 0
Conventions: CF-1.7
institution: French Weather Service - Toulouse
history: 2026-09-10T16:32 GRIB to CDM+CF via cfgrib-0.9.1...Preprocessing for use in pycontrails¶
Both pressure-level and single-level variables require preprocessing for use in pycontrails.
[7]:
import numpy as np
from pycontrails.core.met_var import (
AirTemperature,
EastwardWind,
MassFractionOfCloudIceInAir,
NorthwardWind,
SpecificHumidity,
TOANetDownwardShortwaveFlux,
TOAOutgoingLongwaveFlux,
VerticalVelocity,
)
from pycontrails.physics import thermo
For pressure-level variables, specific humidity must be computed from relative humidity,
[8]:
pl["q"] = 0.01 * pl["r"] * thermo.q_sat_ice(pl["t"], pl["isobaricInhPa"].astype(np.float32) * 100.0)
"step" and "isobaricInhPa" coordinates must be replaced by "time" and "level" coordinates,
[9]:
pl = pl.assign_coords(step=pl["valid_time"])
pl = pl.drop_vars(["time", "valid_time"])
pl = pl.rename(step="time", isobaricInhPa="level")
provenance attributes must be provided,
[10]:
pl.attrs.update({"provider": "Meteo-France", "dataset": "ARPEGE", "product": "forecast"})
and variable names and attributes must be updated to align with pycontrails conventions.
[11]:
pl_vars = {
"u": EastwardWind,
"v": NorthwardWind,
"w": VerticalVelocity,
"t": AirTemperature,
"q": SpecificHumidity,
"ciwc": MassFractionOfCloudIceInAir,
}
pl = pl[pl_vars.keys()]
for k, v in pl_vars.items():
pl[k].attrs.update(
{"units": v.units, "long_name": v.long_name, "standard_name": v.standard_name}
)
pl = pl.rename({k: v.standard_name for k, v in pl_vars.items()})
[12]:
pl
[12]:
<xarray.Dataset> Size: 29GB
Dimensions: (time: 48, level: 24, latitude: 721,
longitude: 1440)
Coordinates:
* time (time) datetime64[ns] 384B 2026-09-1...
* level (level) float64 192B 1e+03 ... 100.0
* latitude (latitude) float64 6kB 90.0 ... -90.0
* longitude (longitude) float64 12kB 0.0 ... 359.8
Data variables:
eastward_wind (time, level, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
northward_wind (time, level, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
lagrangian_tendency_of_air_pressure (time, level, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
air_temperature (time, level, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
specific_humidity (time, level, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
mass_fraction_of_cloud_ice_in_air (time, level, latitude, longitude) float32 5GB dask.array<chunksize=(1, 24, 721, 1440), meta=np.ndarray>
Attributes:
GRIB_edition: 2
GRIB_centre: lfpw
GRIB_centreDescription: French Weather Service - Toulouse
GRIB_subCentre: 0
Conventions: CF-1.7
institution: French Weather Service - Toulouse
history: 2026-09-10T16:32 GRIB to CDM+CF via cfgrib-0.9.1...
provider: Meteo-France
dataset: ARPEGE
product: forecastFor single-level variables, the "step" coordinate must be replaced by a "time" coordinate and a singleton "level" coordinate must be added,
[13]:
sl = sl.squeeze().assign_coords(step=sl["valid_time"])
sl = sl.drop_vars(["valid_time", "surface", "nominalTop"])
sl = sl.rename(step="time", time="init")
sl = sl.expand_dims("level", axis=1).assign_coords(level=[-1.0])
accumulated fluxes must be converted to time averages,
[14]:
# expand time coordinate to include zero accumulated fluxes at forecast initialization
expanded = np.insert(sl["time"].values, 0, sl["init"].values)
sl = sl.reindex(time=expanded, fill_value=0.0)
[15]:
# conversion drops initial step and shifts remaining steps backward by 30 minutes
dt = sl["time"].diff("time")
dt_s = (dt / np.timedelta64(1, "s")).astype(np.float32)
sl = sl.diff("time")
sl["ttr"] = sl["ttr"] / dt_s
sl["tsr"] = sl["tsr"] / dt_s
sl = sl.assign_coords(time=sl["time"] - 0.5 * dt)
the sign of the TOA net longwave flux must be reversed so that values are positive upward,
[16]:
sl["ttr"] = -sl["ttr"]
provenance attributes must be provided,
[17]:
sl.attrs.update({"provider": "Meteo-France", "dataset": "ARPEGE", "product": "forecast"})
and variable names and attributes must be updated to align with pycontrails conventions.
[18]:
sl_vars = {
"ttr": TOAOutgoingLongwaveFlux,
"tsr": TOANetDownwardShortwaveFlux,
}
sl = sl[sl_vars.keys()]
for k, v in sl_vars.items():
sl[k].attrs.update(
{"units": v.units, "long_name": v.long_name, "standard_name": v.standard_name}
)
sl = sl.rename({k: v.standard_name for k, v in sl_vars.items()})
[19]:
sl
[19]:
<xarray.Dataset> Size: 390MB
Dimensions: (time: 47, level: 1, latitude: 721,
longitude: 1440)
Coordinates:
* time (time) datetime64[ns] 376B 2026-09-10T00...
* level (level) float64 8B -1.0
* latitude (latitude) float64 6kB 90.0 89.75 ... -90.0
* longitude (longitude) float64 12kB 0.0 0.25 ... 359.8
init datetime64[ns] 8B ...
Data variables:
toa_outgoing_longwave_flux (time, level, latitude, longitude) float32 195MB dask.array<chunksize=(1, 1, 721, 1440), meta=np.ndarray>
toa_net_downward_shortwave_flux (time, level, latitude, longitude) float32 195MB dask.array<chunksize=(1, 1, 721, 1440), meta=np.ndarray>
Attributes:
GRIB_edition: 2
GRIB_centre: lfpw
GRIB_centreDescription: French Weather Service - Toulouse
GRIB_subCentre: 0
Conventions: CF-1.7
institution: French Weather Service - Toulouse
history: 2026-09-10T16:32 GRIB to CDM+CF via cfgrib-0.9.1...
provider: Meteo-France
dataset: ARPEGE
product: forecastRun gridded CoCiP¶
After preprocessing, ARPEGE forecasts can be wrapped in MetDatasets for use in CoCiP simulations. Note that this requires a relatively recent pycontrails version (0.54.6 or later) with support for generic (model-agnostic) meteorology.
[20]:
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import pandas as pd
from matplotlib import colors
import pycontrails
from pycontrails.core import MetDataset
from pycontrails.models.cocipgrid import CocipGrid
from pycontrails.models.ps_model import PSGrid
from pycontrails.physics import units
[21]:
assert pycontrails.__version__ >= "0.54.6"
[22]:
met = MetDataset(pl).wrap_longitude()
rad = MetDataset(sl).wrap_longitude()
source = CocipGrid.create_source(
time=pl["time"][0],
level=units.ft_to_pl(35_000),
lat_step=0.25,
lon_step=0.25,
)
Pycontrails will produce warnings if provenance attributes indicate that met data is from a source not supported by an official datalib. This is intended as a warning to users that they are responsible for ensuring met data is correctly formatted, and is safe to ignore for preprocessed ARPEGE data. The warning can be suppressed by running CoCiP inside a context manager:
import warnings
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message="Unknown", category=UserWarning)
cocip = CocipGrid(met, rad, ...)
result = cocip.eval(source)
No humidity scaling is required when running CoCiP using experimental ARPEGE data, as the forecast produces realistic supersaturation without one.
[23]:
cocip = CocipGrid(
met,
rad,
humidity_scaling=None,
aircraft_performance=PSGrid(),
aircraft_type="B738",
dt_integration="5min",
azimuth=None,
segment_length=None,
dsn_dz_factor=0.665,
show_progress=False,
)
/Users/tristanabbott/Files/Code/contrails/pycontrails/pycontrails/core/met.py:1134: UserWarning: Unknown provider 'Meteo-France'. Data may not be processed correctly. Known providers are ('ECMWF', 'NCEP', 'DWD'). Contact the pycontrails developers if you believe this is an error.
warnings.warn(
/Users/tristanabbott/Files/Code/contrails/pycontrails/pycontrails/core/met.py:1134: UserWarning: Unknown provider 'Meteo-France'. Data may not be processed correctly. Known providers are ('ECMWF', 'NCEP', 'DWD'). Contact the pycontrails developers if you believe this is an error.
warnings.warn(
[24]:
result = cocip.eval(source)
/Users/tristanabbott/Files/Code/contrails/pycontrails/pycontrails/core/met.py:1134: UserWarning: Unknown provider 'Meteo-France'. Data may not be processed correctly. Known providers are ('ECMWF', 'NCEP', 'DWD'). Contact the pycontrails developers if you believe this is an error.
warnings.warn(
/Users/tristanabbott/Files/Code/contrails/pycontrails/pycontrails/core/met.py:1134: UserWarning: Unknown dataset 'ARPEGE'. Data may not be processed correctly. Known datasets are ('ERA5', 'HRES', 'IFS', 'GFS', 'ICON', 'ICON-EU', 'ICON-D2'). Contact the pycontrails developers if you believe this is an error.
warnings.warn(
[25]:
plt.figure(figsize=(12, 4))
ax = plt.subplot(111, projection=ccrs.PlateCarree())
im = ax.pcolormesh(
result["longitude"].values,
result["latitude"].values,
result["ef_per_m"].values.squeeze().T,
shading="nearest",
cmap="RdBu_r",
norm=colors.SymLogNorm(vmin=-5e9, vmax=5e9, linthresh=1e7),
)
ax.coastlines(color="gray", lw=0.5)
plt.colorbar(im, ax=ax, label="EF (J m$^{-1}$)")
t = pd.Timestamp(result["time"].data.item()).strftime("%Y-%m-%d %HZ")
plt.title(f"Gridded CoCiP on experimental ARPEGE forecast, {t}, FL 350");