Skip to article frontmatterSkip to article content

Climate DT Parameter Series Plot- Data Access using DEDL HDA

This notebook authenticates a user with DestinE services, constructs and submits data requests to the DEDL HDA API for Climate Digital Twin projections, polls for availability, downloads GRIB data for multiple years, and visualizes it using EarthKit.

🚀 Launch in JupyterHub
Prerequisites:References:Credit:
  • Earthkit and HDA Polytope used in this context are both packages provided by the European Centre for Medium-Range Weather Forecasts (ECMWF).

Climate DT - Parameter Series Plot- Data Access using DEDL HDA

Contents

  • Objective: This notebook has the aim to show how to how to use the HDA (Harmonized Data Access) API to query and access Climate DT data to plot a parameter series.

  • Data Sources: https://destine.ecmwf.int/climate-change-adaptation-digital-twin-climate-dt/

  • Methods: The data request is performed using HDA REST API. The variable used in this notebook is the “2 metre temperature”, the temperature of air at 2m above the surface of land, sea or in-land waters. Below the main steps covered by this tutorial.

    1. Setup: Import the required libraries.

    2. Order and Download: How to filter and download climate Dt data.

    3. Plot: How to visualize hourly data on single levels data through Earthkit.

  • Prerequisites:

  • Expected Output:

    • 5 grib file files containing the requested data,

    • 5 maps plot of the monthly mean of the 2 metre temperature at different times.

Setup

Import all the required packages.

import destinelab as deauth
import json
import datetime
import importlib.metadata

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import os
from getpass import getpass
from tqdm import tqdm
import time
from datetime import datetime
from urllib.parse import unquote
from time import sleep
from IPython.display import JSON
import ipywidgets as w

Order and Download

Obtain Authentication Token

To access data we need to be authenticated.

Below how to request of an authentication token using the destinelab package.

DESP_USERNAME = input("Please input your DESP username: ")
DESP_PASSWORD = getpass("Please input your DESP password: ")

auth = deauth.AuthHandler(DESP_USERNAME, DESP_PASSWORD)
access_token = auth.get_token()
if access_token is not None:
    print("DEDL/DESP Access Token Obtained Successfully")
else:
    print("Failed to Obtain DEDL/DESP Access Token")

auth_headers = {"Authorization": f"Bearer {access_token}"}
Please input your DESP username:  eum-dedl-user
Please input your DESP password:  ········
Response code: 200
DEDL/DESP Access Token Obtained Successfully

Check if DT access is granted

If DT access is not granted, you will not be able to execute the rest of the notebook.

import importlib
installed_version = importlib.metadata.version("destinelab")
version_number = installed_version.split('.')[1]
if((int(version_number) >= 8 and float(installed_version) < 1) or float(installed_version) >= 1):
    auth.is_DTaccess_allowed(access_token)
DT Output access allowed

HDA Endpoints

HDA API is based on the Spatio Temporal Asset Catalog specification (STAC). When accessing DestinE data through the HDA API, it is useful to define a small set of configuration constants upfront. These typically include:

  • The STAC API endpoint exposed by HDA

  • The collection name

While the collection name can be specified as a constant, it does not need to be known in advance, as available collections can be discovered dynamically using the discovery API.

For this example we want to access the Future Projection obtained using the IFS-NEMO model of the Climate Change Adaptation Digital Twin data. To find the right collection ID to use for querying HDA we can use the free text search offered by the HDA Discovery API searching for, e.g., Climate Change Adaptation Digital Twin, Future Projection and IFS-NEMO: HDA Discovery API

The result of this operation will give us the collection ID and some other useful information like the temporal extent and the available parameters.

HDA_STAC_ENDPOINT="https://hda.data.destination-earth.eu/stac/v2"
print("STAC endpoint: ", HDA_STAC_ENDPOINT)
STAC endpoint:  https://hda.data.destination-earth.eu/stac/v2
HDA_DISCOVERY_ENDPOINT = HDA_STAC_ENDPOINT+'/collections'
print("HDA discovery endpoint: ", HDA_DISCOVERY_ENDPOINT)
HDA discovery endpoint:  https://hda.data.destination-earth.eu/stac/v2/collections

HDA Discovery

discovery_json=(requests.get(HDA_DISCOVERY_ENDPOINT,params = {"q": '"Climate Change Adaptation Digital Twin","Future Projection","IFS-NEMO"'}).json())

#print("Result from the free text search on the HDA Discovery API : ")
#JSON(discovery_json)
print("The discovery result give us:\nthe collection ID : ", discovery_json["collections"][0].get("id"))
print("\nIts time extension : ", discovery_json["collections"][0].get("extent").get("temporal").get("interval"))
#print("\nThe available parameters: ", discovery_json["collections"][0].get("cube:variables").keys())
print("\nThe available parameters: ")
print("(Type in the text box to narrow the list. When only one item remains, its details will appear)")


keys = sorted(discovery_json["collections"][0]["cube:variables"])
txt = w.Text(description="")
out = w.Output()
def run(_):
    q = txt.value.lower()
    with out:
        out.clear_output(); 
        matches = [k for k in keys if q in k.lower()]
        for k in matches:
            print(k)
            if len(matches)==1:            
                var = discovery_json["collections"][0]["cube:variables"][k]
                print(var["description"], "\n")
                print(json.dumps(var["attrs"], indent=2))
                #print(json.dumps(discovery_json["collections"][0]["cube:variables"][k], indent=2))

                
txt.observe(run, names="value"); display(txt, out); run(None)

#print("\nThe information related to each parameter, e.g. the 2 metre temperature: ")
#JSON(discovery_json["collections"][0].get("cube:variables").get("2_metre_temperature(sfc)"))
Loading...
COLLECTION_ID="EO.ECMWF.DAT.D1.DT_CLIMATE.G1.SCENARIOMIP_SSP3-7.0_IFS-NEMO.R1"

Order and download data

After selecting the correct collection, we need to set up a filter to request our data of interest.

We are interested to the ‘2_metre_temperature’, we can retrieve the ‘2_metre_temperature’ parameter details (parameter_ID, leveltype, product_type) using the Discovery API and the text box in the cell above to narrow down the list to our parameter of interest.

We would like to obtain the temperature in a certain month and day each 4 years, considering the time extension of our collection. With this information, we can:

  • use search to get the precise body and URL needed for the request

  • use order to directly request the data

In this example, we proceed directly with placing the data order using the order endpoint.

Examples on how to use the parameter info to search DT data can be found in the ClimateDT-ParameterPlotter.ipynb or in the ClimateDT-ROISelectionandDataAnalysis.ipynb

datechoice = "20200731"

filter_params = {
        "resolution": "high",      # standard/ high 
        "time": "0000",            # choose the hourly slot(s)
        "type": "fc",              # fixed forecasted fields
        "levtype": "sfc",          # Surface fields (levtype=sfc),
        "param": "167",            # 2m Temperature parameter
        "date":datechoice
    }

In the following cell, we use the collection time extensions—which span 2020 to 2039—to request data for a specific month and day.

For the selected date, we fetch the 2‑metre temperature every four years across the entire extension period.

To retrieve data, you must first submit an order using the filter parameters defined in the previous cell. Each order is processed asynchronously; once a product becomes available, it can be downloaded. The code below iterates through the collection extension years and submits one data request per year.

# Initialize a list to store filenames
filenames = []

# Define start and end years
start_year = 2020
end_year = 2039

#timeout and step for polling (sec)
TIMEOUT = 300
STEP = 1
ONLINE_STATUS = "online"

# Loop 
for year in range(start_year, end_year+1,4):
    # Create a datetime object 
    obsdate = datetime(year, 7, 31)
    datechoice = obsdate.strftime("%Y%m%d")
    filter_params["date"]=datechoice
    response = requests.post(f"{HDA_STAC_ENDPOINT}/collections/{COLLECTION_ID}/order", json=filter_params, headers=auth_headers)

    if response.status_code != 200:
        print(response.content)
    response.raise_for_status()

    ordered_item = response.json()

    product_id = ordered_item["id"]
    storage_tier = ordered_item["properties"].get("storage:tier", "online")
    order_status = ordered_item["properties"].get("order:status", "unknown")
    federation_backend = ordered_item["properties"].get("federation:backends", [None])[0]

    print(f"Product ordered: {product_id}")
    print(f"Provider: {federation_backend}")
    print(f"Storage tier: {storage_tier} (product must have storage tier \"online\" to be downloadable)")
    print(f"Order status: {order_status}")      

    self_url = f"{HDA_STAC_ENDPOINT}/collections/{COLLECTION_ID}/items/{product_id}"
    item = {}

    for i in range(0, TIMEOUT, STEP):
        print(f"Polling {i + 1}/{TIMEOUT // STEP}")

        response = requests.get(self_url, headers=auth_headers)
        if response.status_code != 200:
            print(response.content)
        response.raise_for_status()
        item = response.json()

        storage_tier = item["properties"].get("storage:tier", ONLINE_STATUS)

        if storage_tier == ONLINE_STATUS:
            download_url = item["assets"]["downloadLink"]["href"]
            print("Product is ready to be downloaded.")
            print(f"Asset URL: {download_url}")
            break
        sleep(STEP)
    else:
        order_status = item["properties"].get("order:status", "unknown")
        print(f"We could not download the product after {TIMEOUT // STEP} tries. Current order status is {order_status}")
    
    response = requests.get(download_url, stream=True, headers=auth_headers)
    response.raise_for_status()

    content_disposition = response.headers.get('Content-Disposition')
    total_size = int(response.headers.get("content-length", 0))
    if content_disposition:
        filename = content_disposition.split('filename=')[1].strip('"')
        filename = unquote(filename)
    else:
        filename = os.path.basename(url)

    # Open a local file in binary write mode and write the content
    print(f"downloading {filename}")

    with tqdm(total=total_size, unit="B", unit_scale=True) as progress_bar:
        with open(filename, 'wb') as f:
            for data in response.iter_content(1024):
                progress_bar.update(len(data))
                f.write(data)
               
    # Add the filename to the list
    filenames.append(filename)
Product ordered: 697681f8-590c-4d86-be77-7c5254eda429
Provider: dedt_lumi
Storage tier: offline (product must have storage tier "online" to be downloadable)
Order status: ordered
Polling 1/300
Polling 2/300
Product is ready to be downloaded.
Asset URL: https://hda-download.leonardo.data.destination-earth.eu/data/dedt_lumi/EO.ECMWF.DAT.D1.DT_CLIMATE.G1.SCENARIOMIP_SSP3-7.0_IFS-NEMO.R1/697681f8-590c-4d86-be77-7c5254eda429/downloadLink
downloading 697681f8-590c-4d86-be77-7c5254eda429.grib", attachment; 
26.2MB [00:00, 37.1MB/s]
Product ordered: 4e63beab-b993-4817-8564-21d4b35e541a
Provider: dedt_lumi
Storage tier: offline (product must have storage tier "online" to be downloadable)
Order status: ordered
Polling 1/300
Polling 2/300
Product is ready to be downloaded.
Asset URL: https://hda-download.leonardo.data.destination-earth.eu/data/dedt_lumi/EO.ECMWF.DAT.D1.DT_CLIMATE.G1.SCENARIOMIP_SSP3-7.0_IFS-NEMO.R1/4e63beab-b993-4817-8564-21d4b35e541a/downloadLink
downloading 4e63beab-b993-4817-8564-21d4b35e541a.grib", attachment; 
26.2MB [00:00, 39.2MB/s]
Product ordered: 253f5332-f4c2-4886-aa6f-1ec63b985bb3
Provider: dedt_lumi
Storage tier: offline (product must have storage tier "online" to be downloadable)
Order status: ordered
Polling 1/300
Polling 2/300
Product is ready to be downloaded.
Asset URL: https://hda-download.lumi.data.destination-earth.eu/data/dedt_lumi/EO.ECMWF.DAT.D1.DT_CLIMATE.G1.SCENARIOMIP_SSP3-7.0_IFS-NEMO.R1/253f5332-f4c2-4886-aa6f-1ec63b985bb3/downloadLink
downloading 253f5332-f4c2-4886-aa6f-1ec63b985bb3.grib", attachment; 
26.2MB [00:00, 58.5MB/s]
Product ordered: 57e507e4-4fb8-48ad-99c5-de790de833c6
Provider: dedt_lumi
Storage tier: offline (product must have storage tier "online" to be downloadable)
Order status: ordered
Polling 1/300
Polling 2/300
Product is ready to be downloaded.
Asset URL: https://hda-download.leonardo.data.destination-earth.eu/data/dedt_lumi/EO.ECMWF.DAT.D1.DT_CLIMATE.G1.SCENARIOMIP_SSP3-7.0_IFS-NEMO.R1/57e507e4-4fb8-48ad-99c5-de790de833c6/downloadLink
downloading 57e507e4-4fb8-48ad-99c5-de790de833c6.grib", attachment; 
26.2MB [00:00, 38.2MB/s]
Product ordered: 2e4c7260-b5df-420e-af7e-19aa2d18c746
Provider: dedt_lumi
Storage tier: offline (product must have storage tier "online" to be downloadable)
Order status: ordered
Polling 1/300
Polling 2/300
Product is ready to be downloaded.
Asset URL: https://hda-download.lumi.data.destination-earth.eu/data/dedt_lumi/EO.ECMWF.DAT.D1.DT_CLIMATE.G1.SCENARIOMIP_SSP3-7.0_IFS-NEMO.R1/2e4c7260-b5df-420e-af7e-19aa2d18c746/downloadLink
downloading 2e4c7260-b5df-420e-af7e-19aa2d18c746.grib", attachment; 
26.2MB [00:00, 55.9MB/s]

EarthKit

Using EarthKit, we can load the requested datasets and visualize them directly, making it easy to inspect the results and explore the data.

import earthkit.data
import earthkit.plots
# Iterate over filenames
data=[]
for filename in filenames:
    data.append( earthkit.data.from_source("file", filename))
STYLE = earthkit.plots.styles.Style(
    colors="Spectral_r",
    levels=range(-20, 45),
    units="celsius",
    # Extend the colorbar at both ends
    extend="both",
)
figure = earthkit.plots.Figure(rows=3, columns=2, size=(10, 10))

for i, year in enumerate(range(start_year, end_year+1,4)):
    subplot = figure.add_map()
    subplot.contourf(data[i], style=STYLE)
    subplot.title(f"{year} - Climate DT - IFS NEMO {{short_name!u}} {{time:%d %B %Y}}")


figure.title("{variable_name} in {time:%B %d} of {time:%Y}", fontsize=14)

figure.legend(label="{variable_name!l} ({units})")

figure.show()
Loading...

Cleanup

Let’s now remove the downloaded files

for filename in filenames:
    os.remove(filename)