Skip to article frontmatterSkip to article content

HDA Extract Location Values - Tutorial

This notebook shows how to extract timeseries location values from the Destination Earth Climate Digital Twin (DT) via HDA.

🚀 Launch in JupyterHub

Monthly Temperature Comparison: 2026 vs 2036 in Rome

Contents

  • Objective: This notebook has the aim to show how to request Climate DT data selecting locations of Interest via HDA. We take the opportunity to compare monthly average temperature in Rome in 2 different years.

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

  • Methods: The data request is performed using HDA REST API selecting Rome (Italy) as location of Interest. The variable used in this notebook is the Time-mean temperature - avg_t ( https://codes.ecmwf.int/grib/param-db/235130 )

  • Prerequisites:

  • Expected Output:

    • 2 covjson files containing the requested data,

    • 1 plot of the monthly mean temperature for Rome in 2026 and 2036

Setup

Let’s import useful packages

pip install --user --quiet --upgrade destinelab
Note: you may need to restart the kernel to use updated packages.
import destinelab as deauth
import json
import requests
import os
from getpass import getpass
from tqdm import tqdm
import time
from time import sleep
from IPython.display import JSON
import sys
from IPython.display import display, HTML
from datetime import datetime
import pandas as pd
import matplotlib.pyplot as plt
import re

Search for the desired veariable in the desired Climate DT collection

We search for the “Time-mean temperature” variable (https://codes.ecmwf.int/grib/param-db/235130) in the Climate DT HDA collection representing the future projections (https://data.destination-earth.eu/data-portfolio/EO.ECMWF.DAT.D1.DT_CLIMATE.G2.PROJECTIONS_SSP3-7.0_IFS-FESOM.R1), IFS-FESOM model, selecting our time range of interest.

The search result will give us the json payload to place the order via HDA.

To search and access data we need to authenticate using our DESP account

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:  ········
DEDL/DESP Access Token Obtained Successfully
Response code: 200
DEDL/DESP Access Token Obtained Successfully

Check if we can access DT

auth.is_DTaccess_allowed(access_token)
True

2 - Order the Climate DT data

HDA_STAC_ENDPOINT="https://hda.data.destination-earth.eu/stac/v2"
COLLECTION_ID="EO.ECMWF.DAT.D1.DT_CLIMATE.G2.PROJECTIONS_SSP3-7.0_IFS-FESOM.R1"

Rome - 2026

response = requests.post(HDA_STAC_ENDPOINT+"/search", headers=auth_headers, json={
 "collections": [COLLECTION_ID],
    "query":  {
    "ecmwf:resolution":{"eq": "standard"},
    "ecmwf:levtype":{"eq": "pl"},
    "ecmwf:year":{"eq": ["2026"]},
    "ecmwf:month":{"eq": ["1","2","3","4","5","6","7","8","9","10","11","12"]},
    "ecmwf:param":{"eq": ["235130"]},
    "ecmwf:levelist":{"eq": ["1000"]},
    "ecmwf:stream":{"eq": "clmn"},
    "ecmwf:feature": {"eq":{ "type" : "timeseries", "points": [[41.9028, 12.4964]], "time_axis": "month" }
}
    }
})
if(response.status_code!= 200):
    (print(response.text))
response.raise_for_status()

product = response.json()["features"][0]
JSON(product)

link = next((l for l in product.get('links', []) if l.get("rel") == "retrieve"), None)

if link:
    href = link.get("href")
    body = link.get("body")   # optional: depends on extension
    print("order endpoint:", href)
    print("order body:")
    print(json.dumps(body, indent=4))
else:
    print(f"No link with rel='{target_rel}' found")
order endpoint: https://hda.data.destination-earth.eu/stac/v2/collections/EO.ECMWF.DAT.D1.DT_CLIMATE.G2.PROJECTIONS_SSP3-7.0_IFS-FESOM.R1/order
order body:
{
    "activity": "projections",
    "class": "d1",
    "dataset": "climate-dt",
    "experiment": "SSP3-7.0",
    "expver": "0001",
    "feature": {
        "points": [
            [
                41.9028,
                12.4964
            ]
        ],
        "time_axis": "month",
        "type": "timeseries"
    },
    "generation": "2",
    "levelist": [
        "1000"
    ],
    "levtype": "pl",
    "model": "IFS-FESOM",
    "month": [
        "1",
        "10",
        "11",
        "12",
        "2",
        "3",
        "4",
        "5",
        "6",
        "7",
        "8",
        "9"
    ],
    "param": [
        "235130"
    ],
    "realization": "1",
    "resolution": "standard",
    "stream": "clmn",
    "type": "fc",
    "year": [
        "2026"
    ]
}
response = requests.post(href, json=body, headers=auth_headers)

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

ordered_item = response.json()

product_id = ordered_item["id"]
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"Order status: {order_status}")    
Product ordered: ef876c8a-5307-4fc9-bea6-c19bfec36d3f
Provider: dedt_lumi
Order status: ordered

response = requests.post(href, json=body, 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"Order status: {order_status}")   


#timeout and step for polling (sec)
TIMEOUT = 300
STEP = 1
ORDER_STATUS = "succeeded"

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()
    print(item["properties"].get("order:status"))
    status = item["properties"].get("order:status")

    if status == ORDER_STATUS:
        download_url = item["assets"]["downloadLink"]["href"]
        print("Product is ready to be downloaded.")
        print(f"Download URL: {download_url}")
        break

    time.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}")
 
Order status: ordered
Polling 1/300
ordered
Polling 2/300
succeeded
Product is ready to be downloaded.
Download URL: https://hda-download.central.data.destination-earth.eu/data/dedt_lumi/EO.ECMWF.DAT.D1.DT_CLIMATE.G2.PROJECTIONS_SSP3-7.0_IFS-FESOM.R1/ab08cb71-ce40-4a23-9acb-b4b1e4940e48/downloadLink
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:
    ext = re.search(r'\.(\w+)', content_disposition).group(0)
    filename = '2t_rome_2026'+ext
else:
    filename = os.path.basename(product_id)

# 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)
downloading 2t_rome_2026.covjson
1.54kB [00:00, 5.55MB/s]

Rome - 2036

response = requests.post(HDA_STAC_ENDPOINT+"/search", headers=auth_headers, json={
 "collections": [COLLECTION_ID],
    "query":  {
    "ecmwf:resolution":{"eq": "standard"},
    "ecmwf:levtype":{"eq": "pl"},
    "ecmwf:year":{"eq": ["2036"]},
    "ecmwf:month":{"eq": ["1","2","3","4","5","6","7","8","9","10","11","12"]},
    "ecmwf:param":{"eq": ["235130"]},
    "ecmwf:levelist":{"eq": ["1000"]},
    "ecmwf:stream":{"eq": "clmn"},
    "ecmwf:feature": {"eq":{ "type" : "timeseries", "points": [[41.9028, 12.4964]], "time_axis": "month" }
}
    }
})
if(response.status_code!= 200):
    (print(response.text))
response.raise_for_status()

product = response.json()["features"][0]
JSON(product)

link = next((l for l in product.get('links', []) if l.get("rel") == "retrieve"), None)

if link:
    href = link.get("href")
    body = link.get("body")   # optional: depends on extension
    print("order endpoint:", href)
    print("order body:")
    print(json.dumps(body, indent=4))
else:
    print(f"No link with rel='{target_rel}' found")
order endpoint: https://hda.data.destination-earth.eu/stac/v2/collections/EO.ECMWF.DAT.D1.DT_CLIMATE.G2.PROJECTIONS_SSP3-7.0_IFS-FESOM.R1/order
order body:
{
    "activity": "projections",
    "class": "d1",
    "dataset": "climate-dt",
    "experiment": "SSP3-7.0",
    "expver": "0001",
    "feature": {
        "points": [
            [
                41.9028,
                12.4964
            ]
        ],
        "time_axis": "month",
        "type": "timeseries"
    },
    "generation": "2",
    "levelist": [
        "1000"
    ],
    "levtype": "pl",
    "model": "IFS-FESOM",
    "month": [
        "1",
        "10",
        "11",
        "12",
        "2",
        "3",
        "4",
        "5",
        "6",
        "7",
        "8",
        "9"
    ],
    "param": [
        "235130"
    ],
    "realization": "1",
    "resolution": "standard",
    "stream": "clmn",
    "type": "fc",
    "year": [
        "2036"
    ]
}
response = requests.post(href, json=body, headers=auth_headers)

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

ordered_item = response.json()

product_id = ordered_item["id"]
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"Order status: {order_status}")    
Product ordered: 113fe914-abdf-4f64-bb29-1cc607b8288b
Provider: dedt_lumi
Order status: ordered

response = requests.post(href, json=body, 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"Order status: {order_status}")   


#timeout and step for polling (sec)
TIMEOUT = 300
STEP = 1
ORDER_STATUS = "succeeded"

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()
    print(item["properties"].get("order:status"))
    status = item["properties"].get("order:status")

    if status == ORDER_STATUS:
        download_url = item["assets"]["downloadLink"]["href"]
        print("Product is ready to be downloaded.")
        print(f"Download URL: {download_url}")
        break

    time.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}")
 
Order status: ordered
Polling 1/300
ordered
Polling 2/300
ordered
Polling 3/300
succeeded
Product is ready to be downloaded.
Download URL: https://hda-download.central.data.destination-earth.eu/data/dedt_lumi/EO.ECMWF.DAT.D1.DT_CLIMATE.G2.PROJECTIONS_SSP3-7.0_IFS-FESOM.R1/570f958b-8f4b-4669-984d-040085b54614/downloadLink
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:
    ext = re.search(r'\.(\w+)', content_disposition).group(0)
    filename = '2t_rome_2036'+ext
else:
    filename = os.path.basename(product_id)

# 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)
downloading 2t_rome_2036.covjson
1.54kB [00:00, 3.78MB/s]
def load_covjson(filepath, city):

    with open(filepath, "r") as f:
        data = json.load(f)
    
    cov = data["coverages"][0]
    
    times = cov["domain"]["axes"]["t"]["values"]
    values = cov["ranges"]["avg_t"]["values"]
    
    df = pd.DataFrame({
        "time": pd.to_datetime(times),
        "value_K": values
    })
    df["value_C"] = df["value_K"] - 273.15
    
    df["year"] = df["time"].dt.year
    df["month"] = df["time"].dt.month

    df["city"] = city
    
    return df
rome_2026 = load_covjson("2t_rome_2026.covjson", "Rome_2026")
rome_2036 = load_covjson("2t_rome_2036.covjson", "Rome_2036")

df = pd.concat([rome_2026,rome_2036])

Two years comparison - 2026 - 2036

Differences between years to check if one year is systematically warmer, if both years follow the same seasonal pattern or if there are shifts (e.g. earlier warming)

import matplotlib.pyplot as plt

monthly = df.groupby(["city", "month"])["value_C"].mean().reset_index() #seasonal cycle

for city in ["Rome_2026", "Rome_2036"]:
    subset = monthly[monthly["city"] == city]
    plt.plot(subset["month"], subset["value_C"], marker="o", label=city)

plt.title("Projected Changes in Monthly Mean Temperature — Rome (2026 vs 2036)")
plt.xlabel("Month")
plt.ylabel("Temperature (°C)")
plt.legend()
plt.grid()
plt.show()
<Figure size 640x480 with 1 Axes>