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
Tener una cuenta Earth Data Login
Tener un Bearer Token.
Conocer un collection concept ID (o DOI) de un dataset.
Conexión a internet.
python>=3.12
Objetivos
Usar pydap para demostrar:
Como descubrir URLs de OPeNDAP.
Uso de
tokenspara autenticación (via earthaccess).Acceso a
PACENivel 3 e identificación de un área de interés.Subconjunto y habilitación de la
Constraint Expressionpara 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.9237003El 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.9237003ds["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.9237003print("Size of downloaded subset: ", mds.nbytes/1e9)
Size of downloaded subset: 0.500187072