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.
AC SAF recomend the usage of HARP for reading, filtering, and gridding, as shown in a companion notebook (also 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
The ATBD (Algorithm Theoretical Basis Documents) and PUM (PRODUCT USER MANUAL) are available here: https://acsaf.org/docs/atbd/Algorithm_Theoretical_Basis_Document_NTO_OTO_Nov_2019.pdf and https://acsaf.org/docs/pum/Product_User_Manual_NTO_OTO_Nov_2019.pdf
Data download is described here: https://acsaf.org/offline_access.php and rely on accessing an ftp server.
For this example, the data are included in the zip folder available with this notebook: GOME2A_201812.zip
save then the zip folder into the data folder, at the same level than this notebook
The GOME-2 GDP 4.8/4.9 L2 reprocessed datafiles are structures as one file per orbit, ie 15 files per day:
Several gases are provided into the same orbital file, as mentionned in the file name:
i.e., GOME_O3-NO2-NO2Tropo-BrO-SO2-H2O-HCHO_L2_20181201134102_051_METOPA_62878_DLR_06.HDF5 will include O3, NO2, NO2Tropo, BrO, SO2, H2O and HCHO data.
In the TOTAL_COLUMNS, the final VCD values are given for all the gases, in a order specified in the file name and in the variable /META_DATA/MainSpecies:
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,h5py,os,cf_units
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
import datetime
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
Here we points at a sample day of GOME-2 data on the downloaded and unzipped folder, builds the exact file name for that day/orbit and then lists the matching files on disk as a quick existence check before opening anything.
G2path='./data/GOME2A_201812/'
# ls -lh ./data/GOME2A_201812/GOME_*.HDF5
G2files = glob.glob(G2path + 'GOME_*.HDF5')
# G2file='%s/GOME_O3-NO2-NO2Tropo-BrO-SO2-H2O-HCHO_L2_20181201134102_051_METOPA_62878_DLR_04.HDF5' % G2path
G2file='%s/GOME_O3-NO2-NO2Tropo-BrO-SO2-H2O-HCHO_L2_20181201033250_051_METOPA_62872_DLR_04.HDF5' % G2path
There are several possibilities in python to read an HDF5 file. We show here a few classical ones (with h5py and netCDF4) that preserve the original file structure and then illustate using xarray.
import h5py
f = h5py.File(G2file, "r")
print(list(f.keys()))
['CLOUD_PROPERTIES', 'DETAILED_RESULTS', 'GEOLOCATION', 'META_DATA', 'TOTAL_COLUMNS']
Lists the sub-groups (variable names) inside DETAILED_RESULTS and GEOLOCATION, the two groups used throughout the rest of this notebook, as a quick way to see what's available without opening a separate HDF5 viewer.
print(list(f["DETAILED_RESULTS"].keys()))
['AAI', 'AMFToCloudTop', 'AMFToCloudTop_Error', 'AMFToGround', 'AMFToGround_Error', 'AMFTotal', 'AMFTotal_Error', 'BrO', 'Clouds', 'ESC', 'ESC_Error', 'FittingChiSquare', 'FittingGoodness', 'FittingNumberOfIterations', 'FittingRMS', 'H2O', 'HCHO', 'IntensityWeightedCloudFraction', 'NO2', 'O3', 'QualityFlags', 'SO2', 'SurfaceAlbedo', 'SurfaceConditionFlags', 'SurfaceHeight', 'SurfacePressure', 'VCD', 'VCD_Error']
print(list(f["GEOLOCATION"].keys()))
['EarthRadius', 'IndexInScan', 'InternalPixelId', 'LatitudeA', 'LatitudeB', 'LatitudeC', 'LatitudeCentre', 'LatitudeD', 'LineOfSightZenithAngleCentre', 'LineOfSightZenithAngleE', 'LineOfSightZenithAngleG', 'LineOfSightZenithAngleSatCentre', 'LineOfSightZenithAngleSatE', 'LineOfSightZenithAngleSatG', 'LongitudeA', 'LongitudeB', 'LongitudeC', 'LongitudeCentre', 'LongitudeD', 'OrbitalMode', 'PMDCenterLat', 'PMDCenterLon', 'PMDPixelMap', 'RelativeAzimuthCentre', 'RelativeAzimuthE', 'RelativeAzimuthG', 'RelativeAzimuthSatCentre', 'RelativeAzimuthSatE', 'RelativeAzimuthSatG', 'SAAFlag', 'SatelliteAltitude', 'SolarEclipseFlag', 'SolarZenithAngleCentre', 'SolarZenithAngleE', 'SolarZenithAngleG', 'SolarZenithAngleSatCentre', 'SolarZenithAngleSatE', 'SolarZenithAngleSatG', 'SubPixelInScan', 'SubSatellitePointLatitude', 'SubSatellitePointLongitude', 'SwathMode', 'Time', 'ViewMode']
file = Dataset(G2file, diskless=True, persist=False)
groups = file.groups.keys()
print(groups)
dict_keys(['CLOUD_PROPERTIES', 'DETAILED_RESULTS', 'GEOLOCATION', 'META_DATA', 'TOTAL_COLUMNS'])
Reused from an EUMETSAT example notebook (credited in the comment): pretty-prints every group's variable names, 3 per line, so the whole file structure can be scanned at a glance via the netCDF4 interface.
# from https://gitlab.eumetsat.int/eumetlab/atmosphere/atmosphere/-/blob/master/20_data_exploration/211_Metop-A_GOME-2_NO2Tropo_L2_load_browse.ipynb
for group in groups:
print('{}:'.format(group))
# variables in a group
variables = sorted(file[group].variables.keys())
# pretty print of variables in 3 columns
for v1, v2, v3 in zip(variables[::3], variables[1::3], variables[2::3]):
print(' - {:<30}- {:<30}- {:<}'.format(v1, v2, v3))
if (len(variables) % 3) == 1:
print(' - {:<30}'.format(variables[-1]))
elif (len(variables) % 3) == 2:
print(' - {:<30}- {:<}'.format(*variables[-2:]))
# white line between one line and another
print()
CLOUD_PROPERTIES: - CloudFraction - CloudFraction_Error - CloudMode - CloudOpticalThickness - CloudOpticalThickness_Error - CloudTopAlbedo - CloudTopAlbedo_Error - CloudTopHeight - CloudTopHeight_Error - CloudTopPressure - CloudTopPressure_Error - CloudType - PMDCloudFraction DETAILED_RESULTS: - AAI - AMFToCloudTop - AMFToCloudTop_Error - AMFToGround - AMFToGround_Error - AMFTotal - AMFTotal_Error - ESC - ESC_Error - FittingChiSquare - FittingGoodness - FittingNumberOfIterations - FittingRMS - IntensityWeightedCloudFraction- QualityFlags - SurfaceAlbedo - SurfaceConditionFlags - SurfaceHeight - SurfacePressure - VCD - VCD_Error GEOLOCATION: - EarthRadius - IndexInScan - InternalPixelId - LatitudeA - LatitudeB - LatitudeC - LatitudeCentre - LatitudeD - LineOfSightZenithAngleCentre - LineOfSightZenithAngleE - LineOfSightZenithAngleG - LineOfSightZenithAngleSatCentre - LineOfSightZenithAngleSatE - LineOfSightZenithAngleSatG - LongitudeA - LongitudeB - LongitudeC - LongitudeCentre - LongitudeD - OrbitalMode - PMDCenterLat - PMDCenterLon - PMDPixelMap - RelativeAzimuthCentre - RelativeAzimuthE - RelativeAzimuthG - RelativeAzimuthSatCentre - RelativeAzimuthSatE - RelativeAzimuthSatG - SAAFlag - SatelliteAltitude - SolarEclipseFlag - SolarZenithAngleCentre - SolarZenithAngleE - SolarZenithAngleG - SolarZenithAngleSatCentre - SolarZenithAngleSatE - SolarZenithAngleSatG - SubPixelInScan - SubSatellitePointLatitude - SubSatellitePointLongitude - SwathMode - ViewMode META_DATA: - FWLowerBound - FWName - FWUpperBound - MainSpecies - SolarShifts - VCDQualityIndicator TOTAL_COLUMNS: - BrO - BrO_Error - H2O - H2O_Error - HCHO - HCHO_Error - NO2 - NO2Tropo - NO2Tropo_Error - NO2_Error - O3 - O3_Error - SO2 - SO2_Error
import xarray as xr
depending on the xarray version number, the ingestion of structured files (ie containing groups and not only variables) is possible easelly, or more complicated. We show here how to read part of the file, focusing here on the 'DETAILED_RESULTS/NO2' group.
Please note that more recent xarray versions allow reading the full file with:
ds = xr.open_dataset(G2file, engine="h5netcdf", decode_cf=False)
no2 = file['DETAILED_RESULTS/NO2']
xr_no2 = xr.open_dataset(xr.backends.NetCDF4DataStore(no2))
xr_no2
<xarray.Dataset>
Dimensions: (phony_dim_7: 15127, phony_dim_8: 24)
Dimensions without coordinates: phony_dim_7, phony_dim_8
Data variables: (12/24)
AMFStrato (phony_dim_7) float32 14.21 13.69 ... 13.46
AMFStratoToCloudTop (phony_dim_7) float32 14.29 13.72 ... 13.46
AMFStratoToCloudTop_Error (phony_dim_7) float32 5.914 5.665 ... 5.659
AMFStratoToGround (phony_dim_7) float32 14.09 13.54 ... -1.0
AMFStratoToGround_Error (phony_dim_7) float32 5.914 5.665 ... -1.0
AMFStrato_Error (phony_dim_7) float32 5.914 5.665 ... 5.659
... ...
VCDInit (phony_dim_7) float32 1.377e+15 ... 5.909e+15
VCDInit_Error (phony_dim_7) float32 7.831e+13 ... 3.409e+14
VCDStrato (phony_dim_7) float32 -1.0 -1.0 ... -1.0 -1.0
VCDStrato_Error (phony_dim_7) float32 -1.0 -1.0 ... -1.0 -1.0
VCDTropo (phony_dim_7) float32 -1.0 -1.0 ... -1.0 -1.0
VCDTropo_Error (phony_dim_7) float32 -1.0 -1.0 ... -1.0 -1.0As we are interested in the Tropospheric vertical column density of Nitrogen Dioxide (NO2), VCDTropo, 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['VCDTropo']
<xarray.DataArray 'VCDTropo' (phony_dim_7: 15127)>
array([-1., -1., -1., ..., -1., -1., -1.], dtype=float32)
Dimensions without coordinates: phony_dim_7
Attributes:
Unit: mol/cm2
Title: Tropospheric vertical column density
FillValue: -1.0
CheckSum: -1601901428
ValueRangeMin: -3.1986359e+16
ValueRangeMax: 3.7352162e+16xr_no2['VCDCorrected']
<xarray.DataArray 'VCDCorrected' (phony_dim_7: 15127)>
array([-1., -1., -1., ..., -1., -1., -1.], dtype=float32)
Dimensions without coordinates: phony_dim_7
Attributes:
Unit: mol/cm2
Title: Total vertical column density of NO2 corrected for tropos...
FillValue: -1.0
CheckSum: -496183353
ValueRangeMin: -2.9840647e+16
ValueRangeMax: 3.9565003e+16Lists the dataset's dimension names and sizes:
xr_no2.dims
Frozen({'phony_dim_7': 15127, 'phony_dim_8': 24})
The 'phony_dim_7' and 'phony_dim_8' are actually dimensions that xarray invents bacause the HDF5 files lack proper NetCDF dimension scales. We could rename them, ie:
ds = ds.rename({"phony_dim_7": "ground_pixel"})
Unlike NetCDF4 files, many satellite HDF5 files do not store dimension scales, so xarray cannot determine which variables share dimensions. The NetCDF/HDF backend therefore invents phony_dim_0, phony_dim_1, ...
We 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['VCDTropo'].attrs
{'Unit': 'mol/cm2',
'Title': 'Tropospheric vertical column density',
'FillValue': -1.0,
'CheckSum': -1601901428,
'ValueRangeMin': -3.1986359e+16,
'ValueRangeMax': 3.7352162e+16}
We now have a DataArrary, but we would need geolocations to plot the data on a map. We can thus get them from the netCDF Dataset (previously called file: file = Dataset(G2file, diskless=True, persist=False))
variable2focus='VCDTropo'
latitude = file['GEOLOCATION/LatitudeCentre']
longitude = file['GEOLOCATION/LongitudeCentre']
no2_da = xr.DataArray(
xr_no2[variable2focus].data,
dims=('ground_pixel'),
coords={
'latitude': ('ground_pixel', latitude[:].data),
'longitude': ('ground_pixel', longitude[:].data)
},
attrs={'long_name': xr_no2[variable2focus].Title, 'units': xr_no2[variable2focus].Unit},
name=xr_no2[variable2focus].name
)
no2_da
<xarray.DataArray 'VCDTropo' (ground_pixel: 15127)>
array([-1., -1., -1., ..., -1., -1., -1.], dtype=float32)
Coordinates:
latitude (ground_pixel) float32 65.81 65.49 65.18 ... -69.5 -69.17 -68.83
longitude (ground_pixel) float32 112.8 112.3 111.9 ... 267.8 267.5 267.2
Dimensions without coordinates: ground_pixel
Attributes:
long_name: Tropospheric vertical column density
units: mol/cm2check the loaded NO2 values:
no2_da.data
array([-1., -1., -1., ..., -1., -1., -1.], dtype=float32)
np.mean(no2_da.data)
213674300000000.0
Here, the filled value is -1 and it is clear that the 'tropospheric_NO2_column_number_density' data are not given for all pixels, as it is not always valid.
As discussed in the PUM:
"The tropospheric NO2 column is only available for measurements with latitude < 70º and SZA < 80º. The error in the retrieved tropospheric NO2 columns can become (very) large for measurements with cloud fractions > 20% (cloud radiance fraction > 50%), and these tropospheric NO2 measurements should normally not be used. Therefore, the NO2Tropo entry in the TOTAL_COLUMNS group is not available for these cloudy conditions. "
This is what we see above, with values put to nan for these cases. These case be filtered out using the NO2_Flag
( For expert users and to facilitate detailed data analyses, the tropospheric NO2 columns for cloudy conditions remain available via the VCDTropo entry in DETAILED_RESULTS/NO2. As discussed in the PUM, several variables are reported in several places in the files: The NO2Tropo entry in the TOTAL_COLUMNS group contains the same value as the VCDTropo entry in DETAILED_RESULTS/NO2. These two entries contain the tropospheric NO2 column density. )
from PUM: keep NO2_Flag=1 or 2. This is translated in HARP into the variable "NO2_column_number_density_validity", and the filtering can be done at the ingestion level:
no2_da
<xarray.DataArray 'VCDTropo' (ground_pixel: 15127)>
array([-1., -1., -1., ..., -1., -1., -1.], dtype=float32)
Coordinates:
latitude (ground_pixel) float32 65.81 65.49 65.18 ... -69.5 -69.17 -68.83
longitude (ground_pixel) float32 112.8 112.3 111.9 ... 267.8 267.5 267.2
Dimensions without coordinates: ground_pixel
Attributes:
long_name: Tropospheric vertical column density
units: mol/cm2Builds a fresh xarray.Dataset from the no2_da DataArray (naming the variable NO2Tropo), attaches the NO2Tropo_Flag quality-flag variable alongside it, adds a few descriptive attributes (author, creation date, per-variable long name/unit), then re-displays the dataset to check the result.
variable2focus='VCDTropo'
variableFlag='NO2Tropo_Flag'
no2_ds = no2_da.to_dataset(name="NO2Tropo")
no2_ds["NO2Tropo_Flag"] = xr_no2[variableFlag]
no2_ds.info
<bound method Dataset.info of <xarray.Dataset>
Dimensions: (ground_pixel: 15127, phony_dim_7: 15127)
Coordinates:
latitude (ground_pixel) float32 65.81 65.49 65.18 ... -69.17 -68.83
longitude (ground_pixel) float32 112.8 112.3 111.9 ... 267.5 267.2
Dimensions without coordinates: ground_pixel, phony_dim_7
Data variables:
NO2Tropo (ground_pixel) float32 -1.0 -1.0 -1.0 -1.0 ... -1.0 -1.0 -1.0
NO2Tropo_Flag (phony_dim_7) int32 8 8 8 8 8 8 8 8 8 8 ... 8 8 8 8 8 8 8 8 8>
# add some attributes:
no2_ds.attrs={'description':"NO2 data from the AC SAF file %s" %G2file,'creation_date':datetime.datetime.now(),
'author':'Gaia Pinardi'} #add global attributes
no2_ds['NO2Tropo'].attrs = {'long_name': xr_no2[variable2focus].Title, 'units': xr_no2[variable2focus].Unit}
no2_ds[variableFlag].attrs = {'long_name': xr_no2[variableFlag].Title, 'units': xr_no2[variableFlag].Unit}
no2_ds
<xarray.Dataset>
Dimensions: (ground_pixel: 15127, phony_dim_7: 15127)
Coordinates:
latitude (ground_pixel) float32 65.81 65.49 65.18 ... -69.17 -68.83
longitude (ground_pixel) float32 112.8 112.3 111.9 ... 267.5 267.2
Dimensions without coordinates: ground_pixel, phony_dim_7
Data variables:
NO2Tropo (ground_pixel) float32 -1.0 -1.0 -1.0 -1.0 ... -1.0 -1.0 -1.0
NO2Tropo_Flag (phony_dim_7) int32 8 8 8 8 8 8 8 8 8 8 ... 8 8 8 8 8 8 8 8 8
Attributes:
description: NO2 data from the AC SAF file ./data/GOME2A_201812//GOME_...
creation_date: 2026-09-28 10:52:16.689543
author: Gaia PinardiWe could built the same dataset from scratch (constructing data_vars/coords/attrs by hand) rather than starting from to_dataset() and assigning variables afterwards. We show it in a commented cell.
# #try to create the dataset from scratch:
# latitude = file['GEOLOCATION/LatitudeCentre']
# longitude = file['GEOLOCATION/LongitudeCentre']
# # define data with variable attributes
# data_vars = {xr_no2[variable2focus].name: ( ('latitude','longitude','ground_pixel'), xr_no2[variable2focus].data,
# {'long_name': xr_no2[variable2focus].Title, 'units': xr_no2[variable2focus].Unit}),
# variableFlag : ( ('latitude','longitude','ground_pixel'), xr_no2[variableFlag].data,
# {'long_name': xr_no2[variableFlag].Title, 'units': xr_no2[variableFlag].Unit} )}
# # define coordinates
# coords={'latitude': ('ground_pixel', latitude[:].data),
# 'longitude': ('ground_pixel', longitude[:].data),
# }
# # define global attributes
# attrs = {'creation_date':datetime.datetime.now(),
# 'author':'Gaia Pinardi',
# 'description':"NO2 data from the AC SAF file %s" %G2file
# }
# no2_ds = xr.Dataset(data_vars=data_vars,
# coords=coords,
# attrs=attrs)
# # # OR add variable attrributes after:
# # attrs=dict(description="NO2 data from the AC SAF file %s" %G2file), #add a global attribute
# # no2_ds[variable2focus].attrs = {'long_name': xr_no2[variable2focus].Title, 'units': xr_no2[variable2focus].Unit}
# # no2_ds[variableFlag].attrs = {'long_name': xr_no2[variableFlag].Title, 'units': xr_no2[variableFlag].Unit}
# no2_ds
to avoid the problem of the different axis ("phony_dim_7" with the same dimension), force the flag (and others) to have the same dimension:
no2_ds["NO2Tropo_Flag"] = xr.DataArray(
xr_no2["NO2Tropo_Flag"].data,
dims=("ground_pixel",)
)
# or in shorter:
no2_ds["NO2Tropo_Flag"] = ("ground_pixel", xr_no2["NO2Tropo_Flag"].data)
Re-displays the dataset now that NO2Tropo_Flag has an explicit ground_pixel dimension matching the other variables (fixing the phony_dim mismatch noted earlier).
no2_ds
<xarray.Dataset>
Dimensions: (ground_pixel: 15127)
Coordinates:
latitude (ground_pixel) float32 65.81 65.49 65.18 ... -69.17 -68.83
longitude (ground_pixel) float32 112.8 112.3 111.9 ... 267.5 267.2
Dimensions without coordinates: ground_pixel
Data variables:
NO2Tropo (ground_pixel) float32 -1.0 -1.0 -1.0 -1.0 ... -1.0 -1.0 -1.0
NO2Tropo_Flag (ground_pixel) int32 8 8 8 8 8 8 8 8 8 ... 8 8 8 8 8 8 8 8 8
Attributes:
description: NO2 data from the AC SAF file ./data/GOME2A_201812//GOME_...
creation_date: 2026-09-28 10:52:16.689543
author: Gaia Pinardinp.unique(no2_ds["NO2Tropo_Flag"].data)
array([ 1, 2, 4, 8, 16], dtype=int32)
as mentionned above, we want to keep only data with the NO2Tropo_Flag set to 1 or 2:
no2_ds["NO2Tropo_filtered"] = no2_ds["NO2Tropo"].where(no2_ds["NO2Tropo_Flag"] <= 2)
Inspects the dataset now that it also holds NO2Tropo_filtered, the NO2 column masked to keep only flag values ≤ 2.
no2_ds.info
<bound method Dataset.info of <xarray.Dataset>
Dimensions: (ground_pixel: 15127)
Coordinates:
latitude (ground_pixel) float32 65.81 65.49 ... -69.17 -68.83
longitude (ground_pixel) float32 112.8 112.3 111.9 ... 267.5 267.2
Dimensions without coordinates: ground_pixel
Data variables:
NO2Tropo (ground_pixel) float32 -1.0 -1.0 -1.0 ... -1.0 -1.0 -1.0
NO2Tropo_Flag (ground_pixel) int32 8 8 8 8 8 8 8 8 ... 8 8 8 8 8 8 8 8
NO2Tropo_filtered (ground_pixel) float32 nan nan nan nan ... nan nan nan
Attributes:
description: NO2 data from the AC SAF file ./data/GOME2A_201812//GOME_...
creation_date: 2026-09-28 10:52:16.689543
author: Gaia Pinardi>
sanity-check for both cloud_fraction and solar_zenith_angle, that are then attached to the dataset as new ground_pixel-indexed variables, and the dataset is redisplayed to confirm.
np.shape(file['CLOUD_PROPERTIES/CloudFraction'])
(15127,)
np.shape(file['GEOLOCATION/SolarZenithAngleCentre'])
(15127,)
no2_ds["cloud_fraction"] = ("ground_pixel",file['CLOUD_PROPERTIES/CloudFraction'][:].data)
no2_ds["solar_zenith_angle"] = ("ground_pixel",file['GEOLOCATION/SolarZenithAngleCentre'][:].data)
no2_ds.info
<bound method Dataset.info of <xarray.Dataset>
Dimensions: (ground_pixel: 15127)
Coordinates:
latitude (ground_pixel) float32 65.81 65.49 ... -69.17 -68.83
longitude (ground_pixel) float32 112.8 112.3 111.9 ... 267.5 267.2
Dimensions without coordinates: ground_pixel
Data variables:
NO2Tropo (ground_pixel) float32 -1.0 -1.0 -1.0 ... -1.0 -1.0 -1.0
NO2Tropo_Flag (ground_pixel) int32 8 8 8 8 8 8 8 8 ... 8 8 8 8 8 8 8 8
NO2Tropo_filtered (ground_pixel) float32 nan nan nan nan ... nan nan nan
cloud_fraction (ground_pixel) float32 0.5715 0.773 ... 0.9768 1.0
solar_zenith_angle (ground_pixel) float32 88.17 87.89 87.62 ... 87.61 87.89
Attributes:
description: NO2 data from the AC SAF file ./data/GOME2A_201812//GOME_...
creation_date: 2026-09-28 10:52:16.689543
author: Gaia Pinardi>
other possibility is to create mask with more complex situations:
mask = (
(no2_ds["NO2Tropo_Flag"] <= 2) &
(no2_ds["cloud_fraction"] < 0.2) &
(no2_ds["solar_zenith_angle"] < 75)
)
no2_ds["NO2Tropo_filtered2"] = no2_ds["NO2Tropo"].where(mask)
and we can also filter all the variables in once, and create a new, filtered dataset:
no2_ds_sel = no2_ds.where(mask)
Re-displays NO2Tropo_filtered, then checks its NaN-aware mean, and finally compares that mean against the boolean-mask version (NO2Tropo_filtered2) and the dataset-level .where(mask) version (no2_ds_sel) — a sanity check that the three filtering approaches agree.
# product['NO2_column_number_density'].data, product['tropospheric_NO2_column_number_density'].data
no2_ds["NO2Tropo_filtered"]
<xarray.DataArray 'NO2Tropo_filtered' (ground_pixel: 15127)>
array([nan, nan, nan, ..., nan, nan, nan], dtype=float32)
Coordinates:
latitude (ground_pixel) float32 65.81 65.49 65.18 ... -69.5 -69.17 -68.83
longitude (ground_pixel) float32 112.8 112.3 111.9 ... 267.8 267.5 267.2
Dimensions without coordinates: ground_pixel
Attributes:
long_name: Tropospheric vertical column density
units: mol/cm2no2_ds["NO2Tropo_filtered"].mean(skipna=True)
<xarray.DataArray 'NO2Tropo_filtered' ()> array(3.9708795e+14, dtype=float32)
We can now compare the three products built so far side by side: the basic loading (the unfiltered product) and the distinct validity-flag values with different extra options. We show the pixel counts that each filter level keeps and confirm e.g. that product_sel/product_sel2 only contain flags ≤ 2.
np.nanmean(no2_ds["NO2Tropo_filtered"]), np.nanmean(no2_ds["NO2Tropo_filtered2"]), np.nanmean(no2_ds_sel["NO2Tropo"])
(397087950000000.0, 419058250000000.0, 419058250000000.0)
To plot the tropospheric NO2 data, we create a function that works on arrays. We provide as input some arguments such as the type of projection, the colorbar type, the colorbar min and max, ...
conversion_factor=1e15
projection=ccrs.PlateCarree()
vmin=0
vmax=1
point_size=20
cmap='viridis' #'jet' #
# initialize to nan lat, lon and var2plot to avoid problems:
# lon = np.full((total_size,nlayers),np.nan,dtype = np.dtype('f4'))
# provide some initial arrays, to use with the plotting function
lat=no2_ds_sel.latitude.data
lon=no2_ds_sel.longitude.data
var2plot=no2_ds_sel['NO2Tropo'].data[:] #*conversion_factor
def plot_map_GOME2(lon=lon, lat=lat,var2plot=var2plot,conversion_factor=1e15,unit='molec/cm2',cmap='viridis',
projection=ccrs.PlateCarree(),point_size=20,vmin=0,vmax=1,title='GOME-2 AC SAF L2'):
"""
fig = plot_map_GOME2(lon=lon, lat=lat,var2plot=var2plot,conversion_factor=1e-15,unit='molec/cm2',cmap='viridis',
projection=ccrs.PlateCarree(),point_size=20,vmin=0,vmax=1,title='GOME-2 AC SAF L2')
plot a global map of the L2 points (as scatter points)
"""
fig=plt.figure(figsize=(20, 10))
ax = plt.axes(projection=projection)
ax.coastlines()
ax.set_global()
# plot pixel positions
img = ax.scatter( lon, lat, c=var2plot/conversion_factor,
cmap=plt.cm.get_cmap(cmap),
marker='o',
s=point_size,
transform=ccrs.PlateCarree(),
vmin=vmin,
vmax=vmax
)
plt.xticks(fontsize=16)
plt.yticks(fontsize=16)
plt.xlabel("Longitude", fontsize=16)
plt.ylabel("Latitude", fontsize=16)
cbar = fig.colorbar(img, ax=ax, orientation='horizontal', fraction=0.04, pad=0.1)
cbar.set_label(str(conversion_factor) + ' ' + unit, fontsize=16)
cbar.ax.tick_params(labelsize=14)
ax.set_title(title, fontsize=20, pad=20.0)
plt.show()
return fig
We call plot_map_GOME2; the next two cells inspect the plotted array and its summary statistics.
# provide some initial arrays, to use with the plotting function
lat=no2_ds_sel.latitude.data
lon=no2_ds_sel.longitude.data
var2plot=no2_ds_sel['NO2Tropo'].data[:] #*conversion_factor
# var2plot = productT.tropospheric_NO2_column_number_density.data
# unit='molec/cm2'
unit=no2_da.units
title=no2_da.long_name
fig = plot_map_GOME2(lon=lon, lat=lat,var2plot=var2plot,conversion_factor=1e15,unit=unit,cmap='viridis',projection=ccrs.PlateCarree(),
point_size=20,vmin=0,vmax=1,title='GOME-2 AC SAF L2 NO2 '+title)
var2plot
array([nan, nan, nan, ..., nan, nan, nan], dtype=float32)
np.nanmin(var2plot), np.nanmax(var2plot), np.nanmean(var2plot), var2plot.mean()
(-2346757800000000.0, 3.7352162e+16, 419058250000000.0, nan)
# End of Notebook