Acceso a datos de clorofila A de PACE mediante OPeNDAP de NASA Earthdata en la nube#

Este notebook demuestra el acceso y el subconjunto de datos PACE Ocean Color. Se puede encontrar información general sobre el dataset en el sitio web de PACE (ver aquí).

Requisitos para ejecutar este notebook

  1. Tener una cuenta Earth Data Login

  2. Tener un Bearer Token.

  3. Conocer un collection concept ID (o DOI) de un dataset.

  4. Conexión a internet.

  5. python>=3.12

Objetivos

Usar pydap para demostrar:

  • Como descubrir URLs de OPeNDAP.

  • Uso de tokens para autenticación (via earthaccess).

  • Acceso a PACE Nivel 3 e identificación de un área de interés.

  • Subconjunto y habilitación de la Constraint Expression para que llegue al servidor OPeNDAP.

  • Almacenar el subconjunto localmente.

Autor: Miguel Jimenez-Urias, ‘25

import xarray as xr
import earthaccess
import datetime as dt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as colors

from pydap.client import get_cmr_urls, consolidate_metadata, open_url
from pydap.net import create_session

Usar una sesión como base de datos de metadatos y autenticación#

Pydap puede usar un objeto requests_cache.CachedSession, que puede servir como:

  • Autenticación (mediante netrc o token).

  • Gestión de base de datos (sqlite3 como backend). Solo cachea los datos necesarios.

  • Transmisión de datos.

Abajo inicializamos la sesión, heredando credenciales de autenticación desde earthaccess, y apuntando a la base de datos local PACE.sqlite almacenada en la carpeta /data.

from earthaccess.exceptions import LoginStrategyUnavailable
try:
    auth = earthaccess.login(strategy="netrc", persist=True) # you will be promted to add your EDL credentials
except LoginStrategyUnavailable:
    auth = earthaccess.login(strategy="interactive", persist=True)

# pass Token Authorization to a new Session.
session = create_session(auth.get_session())

Consultar CMR para URLs OPeNDAP#

Consultamos el CMR de NASA para todas las URLs opendap asociadas con Chlorophyll A mallado, diario, Nivel 3, a 4 km, versión 3.1. Para esto, puedes usar Earthdata Search para identificar el Concept Collection ID. Nos enfocamos en 2 meses de datos de este año.

PACE_ccid = "C3620140255-OB_CLOUD" # version 3.1
time_range=[dt.datetime(2025, 6, 1), dt.datetime(2025, 8, 1)]
urls = get_cmr_urls(ccid=PACE_ccid, limit=1000, time_range=time_range) # limit by default = 50

Note

Esta colección tenía dos versiones de los mismos datos. Nos interesa la resolución 4km. Una forma de asegurarse de que cualquier número de URLs opendap pertenecen a la misma colección es inspeccionando las cadenas de URL (innecesario en la mayoría de los casos).

Además, subdividimos por nombre de variable, reteniendo solo las variables lat, lon y chlor_a. Estas son las variables de interés, y descartar variables reduce el tiempo que tarda Xarray en procesar los metadatos.

Note

Xarray tiene un método .drop_vars, pero estas variables se descartan DESPUÉS de crear el dataset. Si un dataset tiene O(1000) variables, Xarray procesaría primero las variables y luego las descartaría. Con OPeNDAP, puedes indicar al servidor de antemano qué variables necesita procesar Xarray.

CEs = "?dap4.ce=/lat;/lon;/chlor_a"
_urls = [url for url in urls if '4km' in url and "DAY" in url]
pace_urls = [url.replace("https", "dap4") + CEs for url in _urls] # a dap4 schema implies a DAP4 protocol in pydap.
len(pace_urls)
32
pace_urls[:4]
['dap4://oceandata.sci.gsfc.nasa.gov/opendap/PACE_OCI/L3SMI/2025/0701/PACE_OCI.20250701.L3m.DAY.CHL.V3_1.chlor_a.4km.NRT.nc?dap4.ce=/lat;/lon;/chlor_a',
 'dap4://oceandata.sci.gsfc.nasa.gov/opendap/PACE_OCI/L3SMI/2025/0702/PACE_OCI.20250702.L3m.DAY.CHL.V3_1.chlor_a.4km.NRT.nc?dap4.ce=/lat;/lon;/chlor_a',
 'dap4://oceandata.sci.gsfc.nasa.gov/opendap/PACE_OCI/L3SMI/2025/0703/PACE_OCI.20250703.L3m.DAY.CHL.V3_1.chlor_a.4km.NRT.nc?dap4.ce=/lat;/lon;/chlor_a',
 'dap4://oceandata.sci.gsfc.nasa.gov/opendap/PACE_OCI/L3SMI/2025/0704/PACE_OCI.20250704.L3m.DAY.CHL.V3_1.chlor_a.4km.NRT.nc?dap4.ce=/lat;/lon;/chlor_a']

Crear un único objeto Dataset#

Usamos Xarray con pydap como backend. Pydap es un backend que siempre se instala al instalar Xarray, pero aún no es el predeterminado al trabajar con URLs opendap. Por eso debemos definirlo.

%%time
ds = xr.open_mfdataset(
    pace_urls, 
    engine='pydap', 
    session=session, 
    parallel=True, 
    concat_dim='time',  # <------ a time dimension will be created 
    combine='nested',
)
CPU times: user 2.56 s, sys: 363 ms, total: 2.93 s
Wall time: 18.9 s
ds
<xarray.Dataset> Size: 5GB
Dimensions:  (time: 32, lat: 4320, lon: 8640)
Coordinates:
  * lat      (lat) float32 17kB 89.98 89.94 89.9 89.85 ... -89.9 -89.94 -89.98
  * lon      (lon) float32 35kB -180.0 -179.9 -179.9 ... 179.9 179.9 180.0
Dimensions without coordinates: time
Data variables:
    chlor_a  (time, lat, lon) float32 5GB dask.array<chunksize=(1, 4320, 8640), meta=np.ndarray>
Attributes: (12/64)
    product_name:                      PACE_OCI.20250701.L3m.DAY.CHL.V3_1.chl...
    instrument:                        OCI
    title:                             OCI Level-3 Standard Mapped Image
    project:                           Ocean Biology Processing Group (NASA/G...
    platform:                          PACE
    source:                            satellite observations from OCI-PACE
    ...                                ...
    identifier_product_doi:            10.5067/PACE/OCI/L3M/CHL/3.1
    keywords:                          Earth Science > Oceans > Ocean Chemist...
    keywords_vocabulary:               NASA Global Change Master Directory (G...
    data_bins:                         Attribute elided: Unsupported attribut...
    data_minimum:                      0.0134539828
    data_maximum:                      15.9237003

El dataset anterior representa una representación de metadatos de todos los datos de interés. Está fragmentado, y solo se han descargado las dimensiones lat y lon.

Note

Se creó una dimensión con nombre time, junto con todos los gránulos remotos concatenados.

print('Uncompressed dataset size [GBs]: ', ds.nbytes / 1e9)
Uncompressed dataset size [GBs]:  4.77762624

Subconjunto próximo a los datos: ¿cómo hacerlo con OPeNDAP, Pydap y Xarray?#

Aunque es posible descargar todo el dataset, es mejor práctica subdividirlo primero. Hay muchas herramientas para subdividir datos de forma próxima a los datos, y OPeNDAP es una de ellas. Necesitamos identificar primero el subconjunto y pasarlo al servidor.

Identificar el subconjunto permite construir potencialmente Constraint Expressions, que se usan para indicar al servidor OPeNDAP el subconjunto de interés. Pero con Pydap y Xarray, estas herramientas construyen rebanados de forma interactiva y pueden solicitar subconjuntos, ocultando la abstracción. Abajo demostramos cómo pasar el subconjunto al servidor OPeNDAP para que Xarray haga menos trabajo.

Identificar subconjunto espacial#

En este caso nos interesa un subconjunto espacial. Los datos son Nivel 3 (mallados), por lo que latitud y longitud son uniformes. Además, son 1D y ya se descargaron en memoria.

lat, lon = ds['lat'].values, ds['lon'].values

Supongamos que definimos el área de interés#

# Min/max of lon values
minLon, maxLon = -96, 10

# Min/Max of lat values
minLat, maxLat = 6, 70
iLon = np.where((lon>minLon)&(lon < maxLon))[0]
iLat= np.where((lat>minLat)&(lat < maxLat))[0]

Así que los rebanados son simplemente el primer y último elemento de iLon e iLat:

lon_slice = slice(iLon[0], iLon[-1])
lat_slice = slice(iLat[0], iLat[-1])

Warning

No todos los valores de arreglos de coordenadas lat/lon son monotónicos. Asegúrate siempre de que lo sean, incluso cuando los datos sean Nivel 3 o Nivel 4.

Descargar solo el subconjunto#

Con Xarray, la sintaxis de subconjunto es similar a pandas. Por ejemplo:

ds.isel(lon=lon_slice, lat_slice)

Produce un subconjunto del lado del cliente. Sin embargo:

Warning

Cuando se usa opendap, rebanar como arriba y luego descargar datos solo subdividirá por variable, descargando en la mayoría de los casos todos los datos, para después volver a subdividirlos con xarray.

Para asegurar que los rebanados se envíen al servidor remoto, debemos aplicar chunk the dataset al crearlo. Esto garantizará que el subconjunto se haga principalmente del lado del servidor. DEBES elegir el tamaño de chunk para que coincida con el tamaño del rebanado, como se muestra abajo.

%%time
ds = xr.open_mfdataset(
    pace_urls, 
    engine='pydap', 
    session=session, 
    parallel=True, 
    concat_dim='time',  # <------ a time dimension will be created 
    combine='nested',
    chunks = {'lon': len(iLon), 'lat':len(iLat)} #  <----------- This instructs the OPeNDAP server to subset in space
)
ds
CPU times: user 1.39 s, sys: 215 ms, total: 1.6 s
Wall time: 11 s
<xarray.Dataset> Size: 5GB
Dimensions:  (time: 32, lat: 4320, lon: 8640)
Coordinates:
  * lat      (lat) float32 17kB 89.98 89.94 89.9 89.85 ... -89.9 -89.94 -89.98
  * lon      (lon) float32 35kB -180.0 -179.9 -179.9 ... 179.9 179.9 180.0
Dimensions without coordinates: time
Data variables:
    chlor_a  (time, lat, lon) float32 5GB dask.array<chunksize=(1, 1536, 2544), meta=np.ndarray>
Attributes: (12/64)
    product_name:                      PACE_OCI.20250701.L3m.DAY.CHL.V3_1.chl...
    instrument:                        OCI
    title:                             OCI Level-3 Standard Mapped Image
    project:                           Ocean Biology Processing Group (NASA/G...
    platform:                          PACE
    source:                            satellite observations from OCI-PACE
    ...                                ...
    identifier_product_doi:            10.5067/PACE/OCI/L3M/CHL/3.1
    keywords:                          Earth Science > Oceans > Ocean Chemist...
    keywords_vocabulary:               NASA Global Change Master Directory (G...
    data_bins:                         Attribute elided: Unsupported attribut...
    data_minimum:                      0.0134539828
    data_maximum:                      15.9237003
ds["chlor_a"] ## inspect the chunk of the data
<xarray.DataArray 'chlor_a' (time: 32, lat: 4320, lon: 8640)> Size: 5GB
dask.array<concatenate, shape=(32, 4320, 8640), dtype=float32, chunksize=(1, 1536, 2544), chunktype=numpy.ndarray>
Coordinates:
  * lat      (lat) float32 17kB 89.98 89.94 89.9 89.85 ... -89.9 -89.94 -89.98
  * lon      (lon) float32 35kB -180.0 -179.9 -179.9 ... 179.9 179.9 180.0
Dimensions without coordinates: time
Attributes:
    long_name:      Chlorophyll Concentration, OCI Algorithm
    units:          mg m^-3
    standard_name:  mass_concentration_of_chlorophyll_in_sea_water
    valid_min:      0.00100000005
    valid_max:      100.0
    reference:      Hu, C., Lee Z., and Franz, B.A. (2012). Chlorophyll-a alg...
    display_scale:  log
    display_min:    0.00999999978
    display_max:    20.0
    Maps:           ('/lat', '/lon')

Ahora nos aseguramos de que Xarray trate este subconjunto como un chunk individual (por variable).

ds = ds.isel(lon=slice(iLon[0], iLon[-1]+1), lat=slice(iLat[0], iLat[-1]+1))

Finalmente#

Almacenar datos localmente como NetCDF4. En esta etapa:

  • Los datos se descargan (subdivididos espacialmente mediante OPeNDAP)

  • Los datos se remuestrean (mediante Xarray)

%%time
ds.to_netcdf("data/pace_subset.nc4", mode='w')
CPU times: user 14 s, sys: 8.15 s, total: 22.2 s
Wall time: 31.4 s

Inspeccionamos nuestros datos#

mds = xr.open_dataset("data/pace_subset.nc4")
mds
<xarray.Dataset> Size: 500MB
Dimensions:  (time: 32, lat: 1536, lon: 2544)
Coordinates:
  * lat      (lat) float32 6kB 69.98 69.94 69.9 69.85 ... 6.104 6.062 6.021
  * lon      (lon) float32 10kB -95.98 -95.94 -95.9 -95.85 ... 9.896 9.938 9.979
Dimensions without coordinates: time
Data variables:
    chlor_a  (time, lat, lon) float32 500MB ...
Attributes: (12/64)
    product_name:                      PACE_OCI.20250701.L3m.DAY.CHL.V3_1.chl...
    instrument:                        OCI
    title:                             OCI Level-3 Standard Mapped Image
    project:                           Ocean Biology Processing Group (NASA/G...
    platform:                          PACE
    source:                            satellite observations from OCI-PACE
    ...                                ...
    identifier_product_doi:            10.5067/PACE/OCI/L3M/CHL/3.1
    keywords:                          Earth Science > Oceans > Ocean Chemist...
    keywords_vocabulary:               NASA Global Change Master Directory (G...
    data_bins:                         Attribute elided: Unsupported attribut...
    data_minimum:                      0.0134539828
    data_maximum:                      15.9237003
print("Size of downloaded subset: ", mds.nbytes/1e9)
Size of downloaded subset:  0.500187072