This notebook is an AC SAF notebook prepared by BIRA-IASB (Gaia Pinardi), on illustrating the use of:
A sample zip file is provided with this notebook and instructions on how to register and download more data from the AC SAF archive are also provided below.
Companion notebooks on L2 and on L3 using STAC service are available here: https://acsaf.org/notebooks.php.
The GOME-2/MetOp total column products are generated at DLR in the framework of EUMETSAT’s AC SAF.
For information and access to all AC SAF products, please refer to the AC SAF web page and help desk: AC SAF Web page: http://acsaf.org AC SAF Helpdesk: helpdesk@acsaf.org
Additional information can be found in EUMETSAT's product Navigator accessible through the following link: http://navigator.eumetsat.int
Current and historical quick look images as well as value added products generated by DLR are available at: http://atmos.eoc.dlr.de/gome2
Data download is described here: https://acsaf.org/offline_access.php and rely on accessing an ftp server after registration on the acsaf.org website.
For this example, one netcdf file is included in the zip folder available with this notebook: GOME_NO2_L3_20250729_METOPC_DLR_v1.nc
It has been downloaded from acsaf.eoc.dlr.de, in the following structure: /gome2c/level3/NO2/2025/07
save the file into the data folder, at the same level than this notebook
The GOME-2 NO2 L3 daily file is provided as one netCDF file per day, with the following structure:
Several NO2 componenets (total, stratosheric and tropospheric) are provided into the same file, as well as informations about clouds and surface properties.
Standard library / numerics / IO imports for what follows: h5py/netCDF4/xarray to read the HDF5 file with three different approaches, numpy for arrays, cf_units for unit handling, datetime/warnings as usual helpers (glob for multi-file patterns, matplotlib/cartopy for the maps built later).
import sys,os
import numpy as np
import datetime as dt
import warnings
#makes the plots interactive ...
#%matplotlib widget
import glob
from netCDF4 import Dataset
import xarray as xr
from matplotlib import pyplot as plt #can also be written as "import matplotlib.pyplot as plt"
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
import cartopy
Here we points at a sample day of GOME-2 data on the downloaded folder, builds the exact file name for that day and then lists the matching files on disk as a quick existence check before opening anything.
G2path='./data/'
# GOME_NO2_L3_20250729_METOPC_DLR_v1.nc
# ls -lh ./data/GOME2A_201812/GOME_*.HDF5
G2files = glob.glob(G2path + 'GOME_NO2*_v1.nc')
G2files
['./data/GOME_NO2_L3_20250729_METOPC_DLR_v1.nc']
G2file=G2files[0]
file = Dataset(G2file, diskless=True, persist=False)
groups = file.groups.keys()
print(groups)
dict_keys(['METADATA', 'PRODUCT'])
file['latitude']
<class 'netCDF4._netCDF4.Variable'>
float32 latitude(lat)
unit: degree north
long_name: latitude of pixel center
unlimited dimensions:
current shape = (720,)
filling on, default _FillValue of 9.969209968386869e+36 used
no2 = file['PRODUCT']
xr_no2 = xr.open_dataset(xr.backends.NetCDF4DataStore(no2))
xr_no2
<xarray.Dataset>
Dimensions: (lat: 720, lon: 1440)
Dimensions without coordinates: lat, lon
Data variables:
no2total (lat, lon) float32 ...
no2total_err (lat, lon) float32 ...
no2total_stddev (lat, lon) float32 ...
no2total_nobs (lat, lon) float32 ...
no2trop (lat, lon) float32 ...
no2trop_err (lat, lon) float32 ...
no2trop_stddev (lat, lon) float32 ...
no2trop_nobs (lat, lon) float32 ...As we are interested in the Tropospheric vertical column density of Nitrogen Dioxide (NO2), no2trop, we can retrieve the parameter using square brackets [] and the name of the parameter of interest. We will get an xarray DataArrary, which has a shape of a one-dimensional vector.
The xarray DataArray also store the variable attributes, like the unit, title, ... of the variable.
xr_no2['no2trop']
<xarray.DataArray 'no2trop' (lat: 720, lon: 1440)>
[1036800 values with dtype=float32]
Dimensions without coordinates: lat, lon
Attributes:
unit: molec/cm2
standard_name: troposphere_mole_content_of_nitrogen_dioxidexr_no2['no2total']
<xarray.DataArray 'no2total' (lat: 720, lon: 1440)>
[1036800 values with dtype=float32]
Dimensions without coordinates: lat, lon
Attributes:
unit: molec/cm2
standard_name: atmosphere_mole_content_of_nitrogen_dioxideWe want to see the variable's metadata (title, unit, fill value, ...), that we can use later to build plot labels and units automatically.
xr_no2['no2trop'].attrs
{'unit': 'molec/cm2',
'standard_name': 'troposphere_mole_content_of_nitrogen_dioxide'}
check the loaded NO2 values:
xr_no2['no2trop'].data
array([[nan, nan, nan, ..., nan, nan, nan],
[nan, nan, nan, ..., nan, nan, nan],
[nan, nan, nan, ..., nan, nan, nan],
...,
[nan, nan, nan, ..., nan, nan, nan],
[nan, nan, nan, ..., nan, nan, nan],
[nan, nan, nan, ..., nan, nan, nan]], dtype=float32)
np.nanmean(xr_no2['no2trop'].data)
462580360000000.0
The advantage of L3 daily files, is that the NO2 data are already pre-filtered for valid data only. Compared to the L2 data discussed in the other notebokks, we don't have to filter out valkues based on flag values.
xr_no2.info
<bound method Dataset.info of <xarray.Dataset>
Dimensions: (lat: 720, lon: 1440)
Dimensions without coordinates: lat, lon
Data variables:
no2total (lat, lon) float32 ...
no2total_err (lat, lon) float32 ...
no2total_stddev (lat, lon) float32 ...
no2total_nobs (lat, lon) float32 ...
no2trop (lat, lon) float32 nan nan nan nan nan ... nan nan nan nan
no2trop_err (lat, lon) float32 ...
no2trop_stddev (lat, lon) float32 ...
no2trop_nobs (lat, lon) float32 ...>
For the moment, I don't have latitude and longitude values loaded as coordinates, yet. Do it:
latitude = file['latitude'][:].data
longitude = file['longitude'][:].data
xr_no2 = xr_no2.assign_coords(
lat=latitude,
lon=longitude
)
xr_no2.info
<bound method Dataset.info of <xarray.Dataset>
Dimensions: (lat: 720, lon: 1440)
Coordinates:
* lat (lat) float32 -89.88 -89.62 -89.38 ... 89.38 89.62 89.88
* lon (lon) float32 -179.9 -179.6 -179.4 ... 179.4 179.6 179.9
Data variables:
no2total (lat, lon) float32 ...
no2total_err (lat, lon) float32 ...
no2total_stddev (lat, lon) float32 ...
no2total_nobs (lat, lon) float32 ...
no2trop (lat, lon) float32 nan nan nan nan nan ... nan nan nan nan
no2trop_err (lat, lon) float32 ...
no2trop_stddev (lat, lon) float32 ...
no2trop_nobs (lat, lon) float32 ...>
To plot the L3 data, we can directly use the xarray plot function. We pre-define some arguments such as the type of projection, the colorbar type, the colorbar min and max, ...
projection=ccrs.PlateCarree()
vmin=0
vmax=6e15
cmap='viridis' #'jet' #
# adding coastline require an cartopy axis: https://tutorial.xarray.dev/fundamentals/04.3_geographic_plotting.html
fig = plt.figure()
ax= plt.subplot(projection=ccrs.PlateCarree())
xr_no2['no2total'].plot(ax=ax,transform=ccrs.PlateCarree(),# usual xarray stuff
cbar_kwargs={"orientation": "horizontal", "shrink": 0.7},
robust=True, cmap=cmap, vmin=vmin, vmax=vmax)
# cbar_kwargs is to put it below the map and not on the right side as default
# the standard_name atrribute is used for the colorbar label
if (cmap=='bwr' or cmap=='RdBu'):
plt.gca().set_facecolor("lightgrey")
ax.coastlines() # cartopy function
plt.title('%s - %s' % ('ACSAF GOME-2C L3, total NO2', '20250729'))
Text(0.5, 1.0, 'ACSAF GOME-2C L3, total NO2 - 20250729')
in the figure above, you see the different orbits
# adding coastline require an cartopy axis: https://tutorial.xarray.dev/fundamentals/04.3_geographic_plotting.html
fig = plt.figure()
ax= plt.subplot(projection=ccrs.PlateCarree())
xr_no2['no2trop'].plot(ax=ax,transform=ccrs.PlateCarree(),# usual xarray stuff
cbar_kwargs={"orientation": "horizontal", "shrink": 0.7},
robust=True, cmap=cmap, vmin=vmin, vmax=vmax)
# cbar_kwargs is to put it below the map and not on the right side as default
# the standard_name atrribute is used for the colorbar label
if (cmap=='bwr' or cmap=='RdBu'):
plt.gca().set_facecolor("lightgrey")
ax.coastlines() # cartopy function
plt.title('%s - %s' % ('ACSAF GOME-2C L3, trop. NO2', '20250729'))
Text(0.5, 1.0, 'ACSAF GOME-2C L3, trop. NO2 - 20250729')
the tropopsheric column is only valid where there are no clouds, which are here the white (masked) part of the orbits
# End of Notebook