Proxy dataset - MTG Sentinel-4 UV-Visible-Near infrared (UVN) spectrometer - Nitrogen Dioxide (NO2)

The geostationary Sentinel-4 mission will provide hourly data on tropospheric constituents over Europe for air quality applications. The target species of the Sentinel-4 mission include key air quality parameters such as nitrogen dioxide, ozone, sulphur dioxide, formaldehyde, glyoxal, and aerosols. These species will be monitored by the Sentinel-4 UVN instrument aboard the MTG Sounder satellite.

This notebook provides you an introduction to European Air Quality Forecasts from the Copernicus Atmosphere Monitoring Service (CAMS) and ERA5 reanalysis data from the Copernicus Climate Change Service (C3S). A combination of these two dataset can be used as a proxy for data from the geostationary Sentinel-4 mission.

The event that this notebook hightlights are the fires in southern Italy and in Greece during August 2021.

Nitrogen dioxide is a useful indicator of smoke presence and transport.

Basic Facts

CAMS European Air Quality Forecasts
Spatial resolution: 0.1° x 0.1°
Spatial coverage: Europe
Temporal resolution: 1-hourly up to leadtime hour 120
Temporal coverage: three-year rolling archive
Data format: GRIB or zipped NetCDF

C3S ERA5 reanalysis
Spatial resolution: 0.25° x 0.25°
Spatial coverage: Global
Temporal resolution: 1-hourly
Temporal coverage: since 1950
Data format: GRIB or NetCDF

How to access the data

Copernicus Atmosphere Data Store (ADS)
CAMS European air quality forecasts are available for download via the Copernicus Atmosphere Data Store (ADS). You will need to create an ADS account here.

Data from the ADS can be downloaded in two ways:

Copernicus Climate Change Service
C3S ERA5 reanalysis are available for download via the Copernicus Climate Data Store (CDS). You will need to create a CDS account here.

Data from the CDS can be downloaded in two ways:


Load required libraries

import os
import xarray as xr
import numpy as np
import netCDF4 as nc
import pandas as pd

# Python libraries for visualization
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.colors
from matplotlib.cm import get_cmap
from matplotlib import animation
from matplotlib.axes import Axes
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
import cartopy.feature as cfeature
from cartopy.mpl.geoaxes import GeoAxes
GeoAxes._pcolormesh_patched = Axes.pcolormesh
from IPython.display import HTML

import warnings
warnings.simplefilter(action = "ignore", category = RuntimeWarning)
warnings.simplefilter(action = "ignore", category = UserWarning)

Load helper functions

%run ../functions.ipynb

CAMS European air quality forecasts

Retrieve CAMS European air quality forecasts from the Atmosphere Data Store

[OPTIONAL] The first step is to programmatically download the Nitrogen Dioxide analysis information as part of the European Air Quality Forecasts dataset. This step is optional because we have already downloaded the data for you, which is why the following code is commented out. The retrieve request below requests the following data:

Date: 7 August 2021
Variable: Nitrogen Dioxide
Model: EURAD-IM
Type: Analysis
Pressure level: [5000, 3000, 2000, 1000, 500, 250, 50, 0]
Spatial resolution: 0.1 deg x 0.1 deg
Temporal resolution: hourly
Format: netcdf

The data is downloaded with the filename 20210807_euradim_no2.nc.

Note: Replace the hashes with your ADS credentials. You can find them here, once you are logged in.

# URL = '###########################'
# KEY = '############################'
'''
import cdsapi
c = cdsapi.Client(url=URL, key=KEY)

c.retrieve(
    'cams-europe-air-quality-forecasts',
    {
        'model': 'euradim',
        'date': '2021-08-07/2021-08-07',
        'format': 'netcdf',
        'type': 'analysis',
        'variable': 'nitrogen_dioxide',
        'level': [
            '0', '1000', '2000',
            '250', '3000', '50',
            '500', '5000',
        ],
        'leadtime_hour': '0',
        'time': [
            '00:00', '01:00', '02:00',
            '03:00', '04:00', '05:00',
            '06:00', '07:00', '08:00',
            '09:00', '10:00', '11:00',
            '12:00', '13:00', '14:00',
            '15:00', '16:00', '17:00',
            '18:00', '19:00', '20:00',
            '21:00', '22:00', '23:00',
        ],
    },
    './20210807_euradim_no2.nc')
'''
"\nimport cdsapi\nc = cdsapi.Client(url=URL, key=KEY)\n\nc.retrieve(\n    'cams-europe-air-quality-forecasts',\n    {\n        'model': 'euradim',\n        'date': '2021-08-07/2021-08-07',\n        'format': 'netcdf',\n        'type': 'analysis',\n        'variable': 'nitrogen_dioxide',\n        'level': [\n            '0', '1000', '2000',\n            '250', '3000', '50',\n            '500', '5000',\n        ],\n        'leadtime_hour': '0',\n        'time': [\n            '00:00', '01:00', '02:00',\n            '03:00', '04:00', '05:00',\n            '06:00', '07:00', '08:00',\n            '09:00', '10:00', '11:00',\n            '12:00', '13:00', '14:00',\n            '15:00', '16:00', '17:00',\n            '18:00', '19:00', '20:00',\n            '21:00', '22:00', '23:00',\n        ],\n    },\n    './20210807_euradim_no2.nc')\n"

Load and browse CAMS European air quality forecasts

The next step is to open the NetCDF file as xarray Dataset. You can use the function open_dataset() to open a single NetCDF file. The dataset has four dimensions (longitude, latitude, level and time). A closer look at the coordinates shows that the longitude coordinates are on a grid from [0, 360]. In a next step, we can bring the longitude values to a [-180, 180] grid.

file_cams = xr.open_dataset('../data/cams/2021/20210807_euradim_no2.nc')
file_cams
<xarray.Dataset>
Dimensions:    (longitude: 700, latitude: 420, level: 8, time: 24)
Coordinates:
  * longitude  (longitude) float32 335.0 335.1 335.2 335.4 ... 44.75 44.85 44.95
  * latitude   (latitude) float32 71.95 71.85 71.75 71.65 ... 30.25 30.15 30.05
  * level      (level) float32 0.0 50.0 250.0 500.0 1e+03 2e+03 3e+03 5e+03
  * time       (time) timedelta64[ns] 00:00:00 01:00:00 ... 22:00:00 23:00:00
Data variables:
    no2_conc   (time, level, latitude, longitude) float32 ...
Attributes:
    title:        NO2 Air Pollutant ANALYSIS at 8 levels
    institution:  Data produced by RIUUK
    source:       Data from EURAD-IM model
    history:      Model EURAD ANALYSIS
    ANALYSIS:     Europe, 20210807+[0H_23H]
    summary:      EURAD model hourly ANALYSIS of NO2 concentration at 8 level...
    project:      MACC-RAQ (http://macc-raq.gmes-atmosphere.eu)

You can use the function assign_coords() to reassign the longitude coordinates and shift the values to a [-180, 180] grid.

file_cams_assign = file_cams.assign_coords(longitude=(((file_cams.longitude + 180) % 360) - 180))
file_cams_assign
<xarray.Dataset>
Dimensions:    (longitude: 700, latitude: 420, level: 8, time: 24)
Coordinates:
  * longitude  (longitude) float32 -24.95 -24.85 -24.75 ... 44.75 44.85 44.95
  * latitude   (latitude) float32 71.95 71.85 71.75 71.65 ... 30.25 30.15 30.05
  * level      (level) float32 0.0 50.0 250.0 500.0 1e+03 2e+03 3e+03 5e+03
  * time       (time) timedelta64[ns] 00:00:00 01:00:00 ... 22:00:00 23:00:00
Data variables:
    no2_conc   (time, level, latitude, longitude) float32 ...
Attributes:
    title:        NO2 Air Pollutant ANALYSIS at 8 levels
    institution:  Data produced by RIUUK
    source:       Data from EURAD-IM model
    history:      Model EURAD ANALYSIS
    ANALYSIS:     Europe, 20210807+[0H_23H]
    summary:      EURAD model hourly ANALYSIS of NO2 concentration at 8 level...
    project:      MACC-RAQ (http://macc-raq.gmes-atmosphere.eu)

Now, we can load the variable no2_conc from the xarray Dataset. You simply specify the name of the variable and bring it into square brackets. The loaded xarray.DataArray offers additional variable attributes, e.g. species, units, value and standard_name.

no2_conc = file_cams_assign['no2_conc']
no2_conc
<xarray.DataArray 'no2_conc' (time: 24, level: 8, latitude: 420, longitude: 700)>
[56448000 values with dtype=float32]
Coordinates:
  * longitude  (longitude) float32 -24.95 -24.85 -24.75 ... 44.75 44.85 44.95
  * latitude   (latitude) float32 71.95 71.85 71.75 71.65 ... 30.25 30.15 30.05
  * level      (level) float32 0.0 50.0 250.0 500.0 1e+03 2e+03 3e+03 5e+03
  * time       (time) timedelta64[ns] 00:00:00 01:00:00 ... 22:00:00 23:00:00
Attributes:
    species:        Nitrogen Dioxide
    units:          µg/m3
    value:          hourly values
    standard_name:  mass_concentration_of_nitrogen_dioxide_in_air

In a next step, we average the Nitrogen Dioxide values over all presure levels. This operation is a simple approximation of the total column Nitrogen Dioxide content, which will be one Level 2 product offered from the Sentinel-4 mission.

no2_tc = no2_conc.mean('level')
no2_tc
<xarray.DataArray 'no2_conc' (time: 24, latitude: 420, longitude: 700)>
array([[[0.02885718, 0.02885804, 0.0297473 , ..., 0.02848925,
         0.02858365, 0.02867749],
        [0.02885718, 0.02885718, 0.02885718, ..., 0.02826796,
         0.02826796, 0.02826796],
        [0.02885718, 0.02885718, 0.02885718, ..., 0.02826796,
         0.02826796, 0.02826796],
        ...,
        [0.03783086, 0.03772481, 0.03759424, ..., 0.41327664,
         0.41327664, 0.41327664],
        [0.03733531, 0.03713872, 0.03713478, ..., 0.41327664,
         0.41327664, 0.44276217],
        [0.03713478, 0.03713478, 0.03713478, ..., 0.3759257 ,
         0.41105676, 0.673456  ]],

       [[0.02971211, 0.02971361, 0.03067681, ..., 0.02863247,
         0.02901028, 0.02938742],
        [0.02971211, 0.02971211, 0.02971211, ..., 0.027757  ,
         0.027757  , 0.027757  ],
        [0.02971211, 0.02971211, 0.02971211, ..., 0.027757  ,
         0.027757  , 0.027757  ],
...
        [0.03395721, 0.0339658 , 0.03399318, ..., 0.13919643,
         0.13919643, 0.13919643],
        [0.03400403, 0.03402226, 0.03402333, ..., 0.13919643,
         0.13919643, 0.14799851],
        [0.03402333, 0.03402333, 0.03402333, ..., 0.1440004 ,
         0.1394818 , 0.21685892]],

       [[0.03620426, 0.03620557, 0.03712845, ..., 0.07221993,
         0.07143309, 0.07064772],
        [0.03620426, 0.03620426, 0.03620426, ..., 0.07404827,
         0.07404827, 0.07404827],
        [0.03620426, 0.03620426, 0.03620426, ..., 0.07404827,
         0.07404827, 0.07404827],
        ...,
        [0.03430216, 0.03388681, 0.03283677, ..., 0.12685238,
         0.12685238, 0.12685238],
        [0.03238934, 0.03163483, 0.03161981, ..., 0.12685238,
         0.12685238, 0.15325025],
        [0.03161981, 0.03161981, 0.03161981, ..., 0.12859286,
         0.12695663, 0.35979608]]], dtype=float32)
Coordinates:
  * longitude  (longitude) float32 -24.95 -24.85 -24.75 ... 44.75 44.85 44.95
  * latitude   (latitude) float32 71.95 71.85 71.75 71.65 ... 30.25 30.15 30.05
  * time       (time) timedelta64[ns] 00:00:00 01:00:00 ... 22:00:00 23:00:00

Let us now visualize the Total Column Nitrogen Dioxide information at 15:00 UTC on 7 August 2021 with the pre-defined function visualize_pcolormesh.

hour = 15
visualize_pcolormesh(data_array=no2_tc.isel(time=hour), 
                     longitude=no2_tc.longitude, 
                     latitude=no2_tc.latitude, 
                     projection=ccrs.PlateCarree(), 
                     color_scale='Spectral_r', 
                     unit= no2_conc.units, 
                     long_name= no2_conc.species + ' at ' + str(hour) + ':00 UTC', 
                     vmin=0, 
                     vmax=10, 
                     lonmin=no2_tc.longitude.min(), 
                     lonmax=no2_tc.longitude.max(), 
                     latmin=no2_tc.latitude.min(), 
                     latmax=no2_tc.latitude.max(), 
                     set_global=False)
(<Figure size 1440x720 with 2 Axes>,
 <GeoAxesSubplot:title={'center':'Nitrogen Dioxide at 15:00 UTC'}>)
../_images/figure6_Sentinel-4_UVN_NO2_32_1.png

Copernicus ERA-5 climate reanalysis

[OPTIONAL] The second step is now to download cloud cover information from the ERA5 climate reanalysis data for the same day, 7 August 2021 from the Copernicus Climate Data Store. This step is optional because we have already downloaded the data for you, which is why the following code is commented out. The retrieve request below downloads the following data:

Date: 7 August 2021
Variable: Total Cloud Cover
Type: Reanalysis
Spatial resolution: 0.25 deg x 0.25 deg
Temporal resolution: hourly
Format: netcdf

The data is downloaded with the filename 20210807_era5_tcc.nc.

Note: Replace the hashes with your CDS credentials. You can find them here, once you are logged in.

# cds_url = '##############################'
# cds_key = '#################################'
'''
c = cdsapi.Client(url=cds_url, key=cds_key)

c.retrieve(
    'reanalysis-era5-single-levels',
    {
        'product_type': 'reanalysis',
        'variable': 'total_cloud_cover',
        'year': '2021',
        'month': '08',
        'day': '07',
        'time': [
            '00:00', '01:00', '02:00',
            '03:00', '04:00', '05:00',
            '06:00', '07:00', '08:00',
            '09:00', '10:00', '11:00',
            '12:00', '13:00', '14:00',
            '15:00', '16:00', '17:00',
            '18:00', '19:00', '20:00',
            '21:00', '22:00', '23:00',
        ],
        'area': [
            70, -25, 30,
            45,
        ],
        'format': 'netcdf',
    },
    './20210807_era5_tcc.nc')
'''
"\nc = cdsapi.Client(url=cds_url, key=cds_key)\n\nc.retrieve(\n    'reanalysis-era5-single-levels',\n    {\n        'product_type': 'reanalysis',\n        'variable': 'total_cloud_cover',\n        'year': '2021',\n        'month': '08',\n        'day': '07',\n        'time': [\n            '00:00', '01:00', '02:00',\n            '03:00', '04:00', '05:00',\n            '06:00', '07:00', '08:00',\n            '09:00', '10:00', '11:00',\n            '12:00', '13:00', '14:00',\n            '15:00', '16:00', '17:00',\n            '18:00', '19:00', '20:00',\n            '21:00', '22:00', '23:00',\n        ],\n        'area': [\n            70, -25, 30,\n            45,\n        ],\n        'format': 'netcdf',\n    },\n    './20210807_era5_tcc.nc')\n"

Load and browse ERA-5 climate reanalysis data

The next step is to open the NetCDF file as xarray Dataset. You can use the function open_dataset() to open a single NetCDF file. The dataset has three dimensions (longitude, latitude and time).

file_cloud_cover = xr.open_dataset('../data/cams/2021/20210807_era5_tcc.nc')
file_cloud_cover
<xarray.Dataset>
Dimensions:    (longitude: 281, latitude: 161, time: 24)
Coordinates:
  * longitude  (longitude) float32 -25.0 -24.75 -24.5 -24.25 ... 44.5 44.75 45.0
  * latitude   (latitude) float32 70.0 69.75 69.5 69.25 ... 30.5 30.25 30.0
  * time       (time) datetime64[ns] 2021-08-07 ... 2021-08-07T23:00:00
Data variables:
    tcc        (time, latitude, longitude) float32 ...
Attributes:
    Conventions:  CF-1.6
    history:      2022-02-02 16:14:14 GMT by grib_to_netcdf-2.23.0: /opt/ecmw...

Now, we can load the variable tcc from the xarray Dataset. You simply specify the name of the variable and bring it into square brackets. The loaded xarray.DataArray offers additional variable attributes, e.g. units, long_name and standard_name.

cloud_cover = file_cloud_cover['tcc']
cloud_cover
<xarray.DataArray 'tcc' (time: 24, latitude: 161, longitude: 281)>
[1085784 values with dtype=float32]
Coordinates:
  * longitude  (longitude) float32 -25.0 -24.75 -24.5 -24.25 ... 44.5 44.75 45.0
  * latitude   (latitude) float32 70.0 69.75 69.5 69.25 ... 30.5 30.25 30.0
  * time       (time) datetime64[ns] 2021-08-07 ... 2021-08-07T23:00:00
Attributes:
    units:          (0 - 1)
    long_name:      Total cloud cover
    standard_name:  cloud_area_fraction

Interpolate ERA-5 data to a spatial resolution of CAMS European air quality forecasts

In a next step, we have to bring both datasets onto the same spatial resolution. For this reason, we resample the ERA5 reanalysis data to the spatial resolution of the CAMS European air quality forecasts, which is 0.1 deg x 0.1 deg.

You can use the function interp() to resample the coordinate of a DataArray. As target latitude and longitude information, you can provide the coordinates from the CAMS data.

cloud_cover_10km = cloud_cover.interp(latitude=no2_conc["latitude"], longitude=no2_conc["longitude"])
cloud_cover_10km
<xarray.DataArray 'tcc' (time: 24, latitude: 420, longitude: 700)>
array([[[       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        [       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        [       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        ...,
        [0.16818032, 0.27308804, 0.3779317 , ..., 0.2933331 ,
         0.23000237, 0.1666813 ],
        [0.28166866, 0.36615285, 0.45058548, ..., 0.47645378,
         0.40946785, 0.34249216],
        [0.39515704, 0.45921767, 0.52323925, ..., 0.65957445,
         0.58893335, 0.518303  ]],

       [[       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        [       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        [       nan,        nan,        nan, ...,        nan,
                nan,        nan],
...
        [0.13331224, 0.13936943, 0.1454229 , ..., 0.23830742,
         0.36562204, 0.4929172 ],
        [0.13315088, 0.14110826, 0.14906079, ..., 0.27876353,
         0.41604507, 0.5533057 ],
        [0.13298951, 0.14284709, 0.15269865, ..., 0.31921965,
         0.4664681 , 0.6136941 ]],

       [[       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        [       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        [       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        ...,
        [0.22098069, 0.20511723, 0.18926343, ..., 0.22686279,
         0.34493572, 0.4629906 ],
        [0.1823511 , 0.17435712, 0.16636801, ..., 0.2435018 ,
         0.36259046, 0.48166096],
        [0.1437215 , 0.143597  , 0.14347258, ..., 0.2601408 ,
         0.3802452 , 0.5003313 ]]], dtype=float32)
Coordinates:
  * time       (time) datetime64[ns] 2021-08-07 ... 2021-08-07T23:00:00
  * latitude   (latitude) float32 71.95 71.85 71.75 71.65 ... 30.25 30.15 30.05
  * longitude  (longitude) float32 -24.95 -24.85 -24.75 ... 44.75 44.85 44.95
Attributes:
    units:          (0 - 1)
    long_name:      Total cloud cover
    standard_name:  cloud_area_fraction

Now, we can also visualize the interpolated total cloud cover for one time step. You can use again the pre-defined function visualize_pcolormesh.

hour = 15
visualize_pcolormesh(data_array=cloud_cover_10km.isel(time=hour), 
                     longitude=cloud_cover_10km.longitude, 
                     latitude=cloud_cover_10km.latitude, 
                     projection=ccrs.PlateCarree(), 
                     color_scale='Blues', 
                     unit= cloud_cover.units, 
                     long_name= cloud_cover_10km.long_name + ' at ' + str(hour) + ':00 UTC', 
                     vmin=0, 
                     vmax=1, 
                     lonmin=cloud_cover_10km.longitude.min(), 
                     lonmax=cloud_cover_10km.longitude.max(), 
                     latmin=cloud_cover_10km.latitude.min(), 
                     latmax=cloud_cover_10km.latitude.max(), 
                     set_global=False)
(<Figure size 1440x720 with 2 Axes>,
 <GeoAxesSubplot:title={'center':'Total cloud cover at 15:00 UTC'}>)
../_images/figure6_Sentinel-4_UVN_NO2_51_1.png

Cloud masking

Based on the total cloud cover information, we want to filter pixels with a cloud cover less than 20%. With the function where, we create below a cloud mask, asigning 1 to pixels with a cloud cover less than 20%.

cloud_mask = xr.where(cloud_cover_10km < 0.2, 1, 0)
cloud_mask
<xarray.DataArray 'tcc' (time: 24, latitude: 420, longitude: 700)>
array([[[0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0],
        ...,
        [1, 0, 0, ..., 0, 0, 1],
        [0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0]],

       [[0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0],
        ...,
        [1, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0]],

       [[0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0],
        ...,
...
        ...,
        [1, 1, 1, ..., 0, 0, 0],
        [1, 1, 1, ..., 0, 0, 0],
        [1, 1, 1, ..., 0, 0, 0]],

       [[0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0],
        ...,
        [1, 1, 1, ..., 0, 0, 0],
        [1, 1, 1, ..., 0, 0, 0],
        [1, 1, 1, ..., 0, 0, 0]],

       [[0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0],
        [0, 0, 0, ..., 0, 0, 0],
        ...,
        [0, 0, 1, ..., 0, 0, 0],
        [1, 1, 1, ..., 0, 0, 0],
        [1, 1, 1, ..., 0, 0, 0]]])
Coordinates:
  * time       (time) datetime64[ns] 2021-08-07 ... 2021-08-07T23:00:00
  * latitude   (latitude) float32 71.95 71.85 71.75 71.65 ... 30.25 30.15 30.05
  * longitude  (longitude) float32 -24.95 -24.85 -24.75 ... 44.75 44.85 44.95

Time coordinates of the CAMS European air quality forecasts are in nanoseconds. We can assign the time coordinates from the ERA-5 reanalysis data, which have a datetime format. This allows us to use the time information in the plotting title and the information is in a readable format.

no2_tc_assign = no2_tc.assign_coords(time=cloud_mask.time)

Now you can apply the cloud mask onto the DataArray with the Nitrogen Dioxide information. This masking process creates the proxy data for the Sentinel-4 L2 NO2 product, which will only provide information for nearly cloud-free pixels.

no2_tc_masked = xr.where(cloud_mask==1, no2_tc_assign, np.nan)
no2_tc_masked
<xarray.DataArray (time: 24, latitude: 420, longitude: 700)>
array([[[       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        [       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        [       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        ...,
        [0.03783086,        nan,        nan, ...,        nan,
                nan, 0.41327664],
        [       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],
...
        [0.03395721, 0.0339658 , 0.03399318, ...,        nan,
                nan,        nan],
        [0.03400403, 0.03402226, 0.03402333, ...,        nan,
                nan,        nan],
        [0.03402333, 0.03402333, 0.03402333, ...,        nan,
                nan,        nan]],

       [[       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        [       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        [       nan,        nan,        nan, ...,        nan,
                nan,        nan],
        ...,
        [       nan,        nan, 0.03283677, ...,        nan,
                nan,        nan],
        [0.03238934, 0.03163483, 0.03161981, ...,        nan,
                nan,        nan],
        [0.03161981, 0.03161981, 0.03161981, ...,        nan,
                nan,        nan]]], dtype=float32)
Coordinates:
  * time       (time) datetime64[ns] 2021-08-07 ... 2021-08-07T23:00:00
  * latitude   (latitude) float32 71.95 71.85 71.75 71.65 ... 30.25 30.15 30.05
  * longitude  (longitude) float32 -24.95 -24.85 -24.75 ... 44.75 44.85 44.95

Visualize Nitrogen Dioxide on 7 August 2021 for one time step

We can now visualize the created proxy resembling Level 2 Total Column Nitrogen Dioxide data collected by the Sentinel-4 mission. You can use again the pre-defined function visualize_pcolormesh.

hour = 13
visualize_pcolormesh(data_array=no2_tc_masked.isel(time=hour), 
                     longitude=no2_tc_masked.longitude, 
                     latitude=no2_tc_masked.latitude, 
                     projection=ccrs.PlateCarree(), 
                     color_scale='Spectral_r', 
                     unit= no2_conc.units, 
                     long_name= 'Simulated Sentinel-4 ' + no2_conc.species + ' \n' + str(no2_tc_masked.isel(time=hour).time.data)[0:10] \
                     + ' at ' + str(no2_tc_masked.isel(time=hour).time.data)[11:16] + ' UTC', 
                     vmin=0, 
                     vmax=10, 
                     lonmin=10,
                     lonmax=30,
                     latmin=35,
                     latmax=45,
                     set_global=False)
(<Figure size 1440x720 with 2 Axes>,
 <GeoAxesSubplot:title={'center':'Simulated Sentinel-4 Nitrogen Dioxide \n2021-08-07 at 13:00 UTC'}>)
../_images/figure6_Sentinel-4_UVN_NO2_64_1.png

Animate Total Column Nitrogen Dioxide over 24 hours

In the last step, you can animate the Nitrogen Dioxide over 24 hours in order to see how the trace gas developed during the 7 August 2021.

You can do animations with matplotlib’s function animation. Jupyter’s function HTML can then be used to display HTML and video content.

The animation function consists of 4 parts:

  • Setting the initial state:
    Here, you define the general plot your animation shall use to initialise the animation. You can also define the number of frames (time steps) your animation shall have.

  • Functions to animate:
    An animation consists of three functions: draw(), init() and animate(). draw() is the function where individual frames are passed on and the figure is returned as image. In this example, the function redraws the plot for each time step. init() returns the figure you defined for the initial state. animate() returns the draw() function and animates the function over the given number of frames (time steps).

  • Create a animate.FuncAnimation object:
    The functions defined before are now combined to build an animate.FuncAnimation object.

  • Play the animation as video:
    As a final step, you can integrate the animation into the notebook with the HTML class. You take the generate animation object and convert it to a HTML5 video with the to_html5_video function

# Setting the initial state:
# 1. Define figure for initial plot
fig, ax = visualize_pcolormesh(data_array=no2_tc_masked[0,:,:],
                               longitude=no2_tc_masked.longitude, 
                               latitude=no2_tc_masked.latitude,
                               projection=ccrs.PlateCarree(),
                               color_scale='Spectral_r', 
                               unit=no2_conc.units,
                               long_name= 'Simulated Sentinel-4 ' + no2_conc.species + ' \n' + str(no2_tc_masked.isel(time=0).time.data)[0:10] \
                               + ' at ' + str(no2_tc_masked.isel(time=0).time.data)[11:16] + ' UTC',
                               vmin=0,
                               vmax=10,
                               lonmin=10, 
                               lonmax=30, 
                               latmin=35, 
                               latmax=45, 
                               set_global=False)

frames = 24

def draw(i):
    img = plt.pcolormesh(no2_tc_masked.longitude, 
                         no2_tc_masked.latitude, 
                         no2_tc_masked[i,:,:], 
                         cmap='Spectral_r', 
                         transform=ccrs.PlateCarree(),
                         vmin=0,
                         vmax=10,
                         shading='auto')
    
    ax.set_title('Simulated Sentinel-4 ' + no2_conc.species + ' \n' + str(no2_tc_masked.isel(time=i).time.data)[0:10] + ' at ' + str(no2_tc_masked.isel(time=i).time.data)[11:16] + ' UTC', fontsize=20, pad=20.0)
    return img


def init():
    return fig


def animate(i):
    return draw(i)

ani = animation.FuncAnimation(fig, animate, frames, interval=800, blit=False,
                              init_func=init, repeat=True)

HTML(ani.to_html5_video())

# The following line which saves the video file is commented out
# ani.save('2021-08-07_S4_NO2.mp4')

plt.close(fig)

Play the animation video as HTML5 video

HTML(ani.to_html5_video())

References

Return to the case study

Monitoring active fires with next-generation satellites: Mediterranean Fires Case Study
Nitrogen dioxide total column