logo

Create Animations from CAMS Data#

This tutorial demonstrates how to create animations from data of the Copernicus Atmosphere Monitoring Service (CAMS). We will use as our example Organic Matter Aerosol Optical Depth (AOD) analysis data from the beginning of August 2021 over North America. This was a time of significant wildfire activity.

Run the tutorial via free cloud platforms: Binder Kaggle Colab

Install and import packages#

!pip install cdsapi
# Import CDS API
import cdsapi

# Libraries for reading and working with multidimensional arrays
import numpy as np
import xarray as xr

# Libraries to assist with animation and visualisations
%matplotlib inline
import matplotlib.pyplot as plt
from matplotlib import animation
import cartopy.crs as ccrs
from IPython.display import HTML

# Disable warnings for data download via API
import urllib3 
urllib3.disable_warnings()

Access and read data#

Data download#

Copy your API key into the code cell below, replacing ####### with your key. (Remember, to access data from the ADS, you will need first to register/login https://ads-beta.atmosphere.copernicus.eu/ and obtain an API key from https://ads-beta.atmosphere.copernicus.eu/how-to-api.)

URL = 'https://ads-beta.atmosphere.copernicus.eu/api'

# Replace the hashtags with your key:
KEY = '###################################'

Here we specify a data directory into which we will download our data and all output files that we will generate:

DATADIR = '.'

For our first plotting example, we will use CAMS Global Atmospheric Composition Forecast data. The code below shows the subset characteristics that we will extract from this dataset for the purpose of this tutorial as an API request.

Note

Before running this code, ensure that you have accepted the terms and conditions. This is something you only need to do once for each CAMS dataset. You will find the option to do this by selecting the dataset in the ADS, then scrolling to the end of the Download data tab.

dataset = "cams-global-atmospheric-composition-forecasts"
request = {
    'variable': ['organic_matter_aerosol_optical_depth_550nm'],
    'date': ['2021-08-01/2021-08-08'],
    'time': ['00:00', '12:00'],
    'leadtime_hour': ['0'],
    'type': ['forecast'],
    'data_format': 'netcdf',
    'area': [80, -150, 25, -50]
}

client = cdsapi.Client(url=URL, key=KEY)
client.retrieve(dataset, request).download(f'{DATADIR}/OAOD_2021-08-01_08.nc')
2024-09-12 13:55:17,669 INFO Request ID is e24adaaf-9fc9-49dc-bf46-f693d79b62dd
2024-09-12 13:55:17,856 INFO status has been updated to accepted
2024-09-12 13:55:19,546 INFO status has been updated to running
2024-09-12 13:59:37,195 INFO Creating download object as as_source with files:
['data_sfc.nc']
2024-09-12 13:59:37,196 INFO status has been updated to successful
'./OAOD_2021-08-01_08.nc'

Read data into xarray object#

For convenience, we create a variable with the name of our downloaded file:

fn = f'{DATADIR}/OAOD_2021-08-01_08.nc'

Now we can read the data into an xarray Dataset, then into a Data Array:

# Create Xarray Dataset
ds = xr.open_dataset(fn)
ds
<xarray.Dataset> Size: 2MB
Dimensions:                  (forecast_period: 1, forecast_reference_time: 16,
                              latitude: 138, longitude: 251)
Coordinates:
  * forecast_period          (forecast_period) timedelta64[ns] 8B 00:00:00
  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 128B 20...
  * latitude                 (latitude) float64 1kB 79.8 79.4 79.0 ... 25.4 25.0
  * longitude                (longitude) float64 2kB -150.0 -149.6 ... -50.0
    valid_time               (forecast_reference_time, forecast_period) datetime64[ns] 128B ...
Data variables:
    omaod550                 (forecast_period, forecast_reference_time, latitude, longitude) float32 2MB ...
Attributes:
    GRIB_centre:             ecmf
    GRIB_centreDescription:  European Centre for Medium-Range Weather Forecasts
    GRIB_subCentre:          0
    Conventions:             CF-1.7
    institution:             European Centre for Medium-Range Weather Forecasts
    history:                 2024-09-12T13:59 GRIB to CDM+CF via cfgrib-0.9.1...
# Create Xarray Data Array
da = ds['omaod550']

Animate data#

To visualise the temporal dimension of this data we will create animations, with each frame corresponding to a time step. These include 00:00 and 12:00 at each day from 1 to 8 August. There are several ways to create animations. We will explore two possibilities: In the first example we will simply redraw each frame, and save the output to separate image files from which we will create an animated gif; in the second example we will generate an HTML5 video.

Create initial state#

To create an animation, we need first to create an initial state for our figure. In our case, this will be a static map showing Organic Matter AOD at the first time step in our data time series.

fig = plt.figure(figsize=(10, 5)) # Define the figure and specify size
ax = plt.subplot(1,1,1, projection=ccrs.PlateCarree()) # Specify plot area & projection
ax.gridlines(draw_labels=True, linewidth=1, color='gray', alpha=0.5, linestyle='--') # Add lat/lon grid
ax.set_title(f'Organic Matter AOD at 550nm, {str(da.forecast_reference_time[0].values)[:-16]}', fontsize=12) # Set figure title
ax.coastlines(color='black') # Add coastlines
im = plt.pcolormesh(da.longitude, da.latitude, da[0,0,:,:], cmap='YlOrRd', vmin=0, vmax=2) # Plot the data
cbar = plt.colorbar(im,fraction=0.046, pad=0.04) # Specify the colourbar
cbar.set_label('Organic Matter AOD at 550nm') # Define the colourbar label
/opt/conda/lib/python3.10/site-packages/cartopy/io/__init__.py:241: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/110m_physical/ne_110m_coastline.zip
  warnings.warn(f'Downloading: {url}', DownloadWarning)
_images/ed31585ed4cd6bf19e4af9cf619c4e34c1a33524f97043a5eb8c6980af6fa124.png

This map clearly shows the high values of Organic Matter AOD which appear to originate from the many wildfires burning across North America in this period.

Set number of frames#

We will now create an animation to visualise all time steps. From the description of the dataset above, we see that the time dimension has 16 entries. The number of frames of the animation in our case is therefore 16.

frames = 16

Create a function that will be called by the animation object#

Here create a function that will be called by FuncAnimation. It takes one argument, which is the number of frames. FuncAnimation will then repeatedly call this function to iterate through the time steps of our data.

The function includes the data array to loop through.

We convert the data from an Xarray object to a Numpy array to enable us to flatten it to one dimension for computational efficiency. This is not an essential step (we could just keep the Xarray Data Array, without flattening dimensions), but it speeds up the time taken to produce the animation.

We also loop through all printed time values in the title of each frame.

def animate(i):
    array = da[:,i,:,:].values
    im.set_array(array.flatten())
    ax.set_title(f'Organic Matter AOD at 550nm, {str(da.forecast_reference_time[i].values)[:-16]}', fontsize=12)

Create animation object#

We now create an animation object by calling FuncAnimation with 4 arguments:

  1. fig is the reference to the figure we created.

  2. animate is the function to call at each frame to update the plot.

  3. frames is the number of frames of the animation.

  4. interval is the time, in milliseconds, between animation frames.

ani = animation.FuncAnimation(fig, animate, frames, interval=150)

Display animation#

Run animation using Javascript#

HTML(ani.to_jshtml())