Acceso a ECCOv4 por medio de EarthData in the Cloud de la NASA#

Este tutorial demuestra acceso al producto ECCOv4 de un modelo numerico global. Para mayor informacion del producto de ECCO, puede ir aqui.

Requisitos

  1. Tener una cuenta de Earth Data Login por medio de la NASA.

  2. Tener un Token valido.

Tambien se puede utilizar el metodo de Nombre de Usuario / Contrasena descrito en el tutorial de Autenticacion

Objectivos

Utilizar pydap para demostrar

  • Acceso to archivos cientificos de la NASA por medio del uso de tokens y EarthData como metodo de autenticacion.

Algunas variables de interes son:

Autor: Miguel Jimenez-Urias, ‘24

import matplotlib.pyplot as plt
import numpy as np
from pydap.net import create_session
from pydap.client import open_url
import xarray as xr
import pydap
print("xarray version: ", xr.__version__)
print("pydap version: ", pydap.__version__)
xarray version:  2026.4.0
pydap version:  3.5.10.dev14+g40484efcb

Acceso a EARTHDATA#

El listado de las variables de este producto pueden encontrarse aqui. En este caso, accederemos a las variables en su malla original, el LL90.

Grid_url = 'https://opendap.earthdata.nasa.gov/providers/POCLOUD/collections/ECCO%20Geometry%20Parameters%20for%20the%20Lat-Lon-Cap%2090%20(llc90)%20Native%20Model%20Grid%20(Version%204%20Release%204)/granules/GRID_GEOMETRY_ECCO_V4r4_native_llc0090'

Autenticacion via .netrc#

Las credenciales son recuperadas automaticamente por pydap.

my_session = create_session()

Alternativa: El use the tokens#

session_extra = {"token": "YourToken"}

# initialize a requests.session object with the token headers. All handled by pydap.
my_session = create_session(session_kwargs=session_extra)

Accesso a los metadatos solamente, por medio de pydap#

pydap aprovecha el protocolo de OPeNDAP, el cual permite la separacion de los metadatos de los valores numericos. Esto permite una inspeccion remota de los datasets.

ds_grid = open_url(Grid_url, session=my_session, protocol="dap4")
ds_grid.tree()
.GRID_GEOMETRY_ECCO_V4r4_native_llc0090.nc
├──dxC
├──PHrefF
├──XG
├──dyG
├──rA
├──hFacS
├──Zp1
├──Zl
├──rAw
├──dxG
├──maskW
├──YC
├──XC
├──maskS
├──YG
├──hFacC
├──drC
├──drF
├──XC_bnds
├──Zu
├──Z_bnds
├──YC_bnds
├──PHrefC
├──rAs
├──Depth
├──dyC
├──SN
├──rAz
├──maskC
├──CS
├──hFacW
├──Z
├──i
├──i_g
├──j
├──j_g
├──k
├──k_l
├──k_p1
├──k_u
├──nb
├──nv
└──tile

Note

PyDAP accesa solo a los metadatos de archivo en el servidor de OPeNDAP, y ningun arreglo numerico ha sido descargardo hasta este punto!

Para descargar los arreglos numericos hay que indexar la variable

Esto es, de la siguiente manera:

# this fetches remote data into a pydap object container
pydap_Array = dataset[<VarName>][:]

donde <VarName> es el nombre de una de las variables. El producto sera una representation “en memoria” de tipo pydap.model.BaseType, el cual permite el acceso a los arreglos de numpy.

Para extraer los valores de la variable remota, hay que ejecutar el siguiente comando

# The `.data` command allows direct access to the Numpy array (e.g. for manipulation)
pydap_Array.data
# lets download some data
Depth = ds_grid['Depth'][:]
print(type(Depth))
<class 'pydap.model.BaseType'>
Depth.attributes
{'_FillValue': 9.969209968e+36,
 'long_name': 'model seafloor depth below ocean surface at rest',
 'units': 'm',
 'coordinate': 'XC YC',
 'coverage_content_type': 'modelResult',
 'standard_name': 'sea_floor_depth_below_geoid',
 'comment': "Model sea surface height (SSH) of 0m corresponds to an ocean surface at rest relative to the geoid. Depth corresponds to seafloor depth below geoid. Note: the MITgcm used by ECCO V4r4 implements 'partial cells' so the actual model seafloor depth may differ from the seafloor depth provided by the input bathymetry file.",
 'coordinates': 'YC XC',
 'origname': 'Depth',
 'fullnamepath': '/Depth',
 'Maps': (),
 '_DAP4_Checksum_CRC32': np.uint32(3811813944)}
Depth.shape, Depth.dims
((13, 90, 90), ['/tile', '/j', '/i'])

Visualizando el fondo oceanico Depth en la malla original del modelo

En este caso, el producto ECCO esta definido en una malla con topologia de un Cubo Esferico. De esta manera, la malla horizontal contiene una dimension extra llamada: tile o face. A continuacion visualizamos la variable en su topologia original

Variable = [Depth[i].data for i in range(13)]
clevels =  np.linspace(0, 6000, 100)
cMap = 'Greys_r'
fig, axes = plt.subplots(nrows=5, ncols=5, figsize=(8, 8), gridspec_kw={'hspace':0.01, 'wspace':0.01})
AXES = [
    axes[4, 0], axes[3, 0], axes[2, 0], axes[4, 1], axes[3, 1], axes[2, 1],
    axes[1, 1], 
    axes[1, 2], axes[1, 3], axes[1, 4], axes[0, 2], axes[0, 3], axes[0, 4],
]
for i in range(len(AXES)):
    AXES[i].contourf(Variable[i], clevels, cmap=cMap)

for ax in np.ravel(axes):
    ax.axis('off')
    plt.setp(ax.get_xticklabels(), visible=False)
    plt.setp(ax.get_yticklabels(), visible=False)

plt.show()
../_images/8deb991839e486f093d257b856cf5d089cf76987d9d0f66604c31bd6f61ab15d.png

Fig. 1. La variable Depth visualizada en una malla horitonzal. Tiles con valores 0-5 tienen un ordenamiento de valores (indiciales) C-ordering, mientras que cualquier arreglo numerico tiles 7-13 siguen el F-ordering. Cualquier arreglo numerico en el arctic cap, tiene un comportamiento de una coordenada polar.

Visualizacion con topologia corregida

fig, axes = plt.subplots(nrows=4, ncols=4, figsize=(8, 8), gridspec_kw={'hspace':0.01, 'wspace':0.01})
AXES_NR = [
    axes[3, 0], axes[2, 0], axes[1, 0], axes[3, 1], axes[2, 1], axes[1, 1],
]
AXES_CAP = [axes[0, 0]]
AXES_R = [
    axes[1, 2], axes[2, 2], axes[3, 2], axes[1, 3], axes[2, 3], axes[3, 3],
]
for i in range(len(AXES_NR)):
    AXES_NR[i].contourf(Variable[i], clevels, cmap=cMap)

for i in range(len(AXES_CAP)):
    AXES_CAP[i].contourf(Variable[6].transpose()[:, ::-1], clevels, cmap=cMap)

for i in range(len(AXES_R)):
    AXES_R[i].contourf(Variable[7+i].transpose()[::-1, :], clevels, cmap=cMap)

for ax in np.ravel(axes):
    ax.axis('off')
    plt.setp(ax.get_xticklabels(), visible=False)
    plt.setp(ax.get_yticklabels(), visible=False)

plt.show()
../_images/57aff92f93cde9a0259585ca5662b6f3488687e2a0b3ec72699e276a03b23821.png

Fig. 2. Visualizacion de la variable Depth en una malla horizontal que approxima una malla horizontal uniforme con ejes lat-lon. Sin embargo, cualquier arreglo numerico en el arctic cap, continua en una malla que approxima una malla en coordenadas polares

Utilizando xarray#

pydap se puede llamar dentro de xarray para acceder a los archivos expuestos mediante el servidor de OPeNDAP. En particular, xarray permite aggregar multiples archivos remotos de opendap. A continuacion demostramos un pequeno ejemplo.

Un URL individual –> un archivo remoto

Below we access remote Temperature and Salinity ECCO data with xarray via (internally) pydap.

baseURL = 'https://opendap.earthdata.nasa.gov/providers/POCLOUD/collections/'
Temp_Salt = "ECCO%20Ocean%20Temperature%20and%20Salinity%20-%20Monthly%20Mean%20llc90%20Grid%20(Version%204%20Release%204)/granules/OCEAN_TEMPERATURE_SALINITY_mon_mean_"
year = '2017-'
month = '01'
end_ = '_ECCO_V4r4_native_llc0090'
Temp_2017 = baseURL + Temp_Salt +year  + month + end_

# Convertimos el URL en un DAP4 opendap url
Temp_2017 = Temp_2017.replace("https", "dap4")
Temp_2017
'dap4://opendap.earthdata.nasa.gov/providers/POCLOUD/collections/ECCO%20Ocean%20Temperature%20and%20Salinity%20-%20Monthly%20Mean%20llc90%20Grid%20(Version%204%20Release%204)/granules/OCEAN_TEMPERATURE_SALINITY_mon_mean_2017-01_ECCO_V4r4_native_llc0090'
pyds = open_url(Temp_2017, session=my_session)
pyds.tree()
.OCEAN_TEMPERATURE_SALINITY_mon_mean_2017-01_ECCO_V4r4_native_llc0090.nc
├──XG
├──Zp1
├──Zl
├──YC
├──XC
├──SALT
├──YG
├──XC_bnds
├──Zu
├──THETA
├──Z_bnds
├──YC_bnds
├──time_bnds
├──Z
├──i
├──i_g
├──j
├──j_g
├──k
├──k_l
├──k_p1
├──k_u
├──nb
├──nv
├──tile
└──time

Para evitar descargas variables que no sean necesariamente de interest, se le puede instruir al Servidor Hyrax que variables requiere uno.

Para esto utilizaremos las Expresiones de Restriccion (CE, por sus siglas en ingles). Este metodo nos ayudara a construir datasets mucho mas simples, y evitar descargar N veces, informacion que no es requerida.

A continuacion demostramos el caso de solo requerir la variable THETA y sus dimensiones.

dims = pyds['/THETA'].dims
Vars = ['/THETA'] + dims

# Below construct Contraint Expression
CE = "?dap4.ce="+(";").join(Vars)
print("Constraint Expression: ", CE)
Constraint Expression:  ?dap4.ce=/THETA;/time;/k;/tile;/j;/i
Temp_2017 = baseURL + Temp_Salt +year  + month + end_ + CE
Temp_2017
'https://opendap.earthdata.nasa.gov/providers/POCLOUD/collections/ECCO%20Ocean%20Temperature%20and%20Salinity%20-%20Monthly%20Mean%20llc90%20Grid%20(Version%204%20Release%204)/granules/OCEAN_TEMPERATURE_SALINITY_mon_mean_2017-01_ECCO_V4r4_native_llc0090?dap4.ce=/THETA;/time;/k;/tile;/j;/i'

Importante#

Ahora aplicamos la CE a todos los possibles URLs the nuestro dataset.

Temp_2017 = [baseURL.replace("https", "dap4") + Temp_Salt + year + f'{i:02}' + end_ + CE for i in range(1, 13)]
Temp_2017[:3]
['dap4://opendap.earthdata.nasa.gov/providers/POCLOUD/collections/ECCO%20Ocean%20Temperature%20and%20Salinity%20-%20Monthly%20Mean%20llc90%20Grid%20(Version%204%20Release%204)/granules/OCEAN_TEMPERATURE_SALINITY_mon_mean_2017-01_ECCO_V4r4_native_llc0090?dap4.ce=/THETA;/time;/k;/tile;/j;/i',
 'dap4://opendap.earthdata.nasa.gov/providers/POCLOUD/collections/ECCO%20Ocean%20Temperature%20and%20Salinity%20-%20Monthly%20Mean%20llc90%20Grid%20(Version%204%20Release%204)/granules/OCEAN_TEMPERATURE_SALINITY_mon_mean_2017-02_ECCO_V4r4_native_llc0090?dap4.ce=/THETA;/time;/k;/tile;/j;/i',
 'dap4://opendap.earthdata.nasa.gov/providers/POCLOUD/collections/ECCO%20Ocean%20Temperature%20and%20Salinity%20-%20Monthly%20Mean%20llc90%20Grid%20(Version%204%20Release%204)/granules/OCEAN_TEMPERATURE_SALINITY_mon_mean_2017-03_ECCO_V4r4_native_llc0090?dap4.ce=/THETA;/time;/k;/tile;/j;/i']

#

Debajo inicializaremos una session que persista, cada vez que descargue datos de servidores de OPeNDAP. En este caso utilizaremost la libraria requests_cache. El comportamiento es casi identico excepto la siguiente celda:

cache_session = create_session(use_cache=True, cache_kwargs={"cache_name":"data/ecco"}) # caching the session
cache_session.cache.clear() # clear all cache to demonstrate the behavior

Warning

A partir de la version de pydap >= 3.5.5, existe el nuevo metodo experimental consolidated_metadata que permite a pydap descargar todos los metadatos, y reusarlos. Este metodo continua en desarrollo y sus propiedades pueden cambiar en las siguientes versiones.

from pydap.client import consolidate_metadata
%%time
consolidate_metadata(Temp_2017, session=cache_session, concat_dim='time')
datacube has dimensions ['i[0:1:89]', 'j[0:1:89]', 'k[0:1:49]', 'tile[0:1:12]'] , and concat dim: `['time']`
CPU times: user 2.27 s, sys: 469 ms, total: 2.74 s
Wall time: 53.2 s
%%time
theta_salt_ds = xr.open_mfdataset(
    Temp_2017, 
    engine='pydap',
    session=cache_session, 
    parallel=True, 
    combine='nested', 
    concat_dim='time',
)
CPU times: user 1.05 s, sys: 141 ms, total: 1.19 s
Wall time: 8.46 s
---------------------------------------------------------------------------
ResponseError                             Traceback (most recent call last)
ResponseError: too many 500 error responses

The above exception was the direct cause of the following exception:

MaxRetryError                             Traceback (most recent call last)
File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/requests/adapters.py:696, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    695 try:
--> 696     resp = conn.urlopen(
    697         method=request.method,
    698         url=url,
    699         body=request.body,  # type: ignore[arg-type]  # urllib3 stubs don't accept Iterable[bytes | str]
    700         headers=request.headers,  # type: ignore[arg-type]  # urllib3#3072
    701         redirect=False,
    702         assert_same_host=False,
    703         preload_content=False,
    704         decode_content=False,
    705         retries=self.max_retries,
    706         timeout=resolved_timeout,
    707         chunked=chunked,
    708     )
    710 except (ProtocolError, OSError) as err:

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/urllib3/connectionpool.py:955, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
    954     log.debug("Retry: %s", url)
--> 955     return self.urlopen(
    956         method,
    957         url,
    958         body,
    959         headers,
    960         retries=retries,
    961         redirect=redirect,
    962         assert_same_host=assert_same_host,
    963         timeout=timeout,
    964         pool_timeout=pool_timeout,
    965         release_conn=release_conn,
    966         chunked=chunked,
    967         body_pos=body_pos,
    968         preload_content=preload_content,
    969         decode_content=decode_content,
    970         **response_kw,
    971     )
    973 return response

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/urllib3/connectionpool.py:955, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
    954     log.debug("Retry: %s", url)
--> 955     return self.urlopen(
    956         method,
    957         url,
    958         body,
    959         headers,
    960         retries=retries,
    961         redirect=redirect,
    962         assert_same_host=assert_same_host,
    963         timeout=timeout,
    964         pool_timeout=pool_timeout,
    965         release_conn=release_conn,
    966         chunked=chunked,
    967         body_pos=body_pos,
    968         preload_content=preload_content,
    969         decode_content=decode_content,
    970         **response_kw,
    971     )
    973 return response

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/urllib3/connectionpool.py:945, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
    944 try:
--> 945     retries = retries.increment(method, url, response=response, _pool=self)
    946 except MaxRetryError:

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/urllib3/util/retry.py:543, in Retry.increment(self, method, url, response, error, _pool, _stacktrace)
    542     reason = error or ResponseError(cause)
--> 543     raise MaxRetryError(_pool, url, reason) from reason  # type: ignore[arg-type]
    545 log.debug("Incremented Retry for (url='%s'): %r", url, new_retry)

MaxRetryError: HTTPSConnectionPool(host='opendap.earthdata.nasa.gov', port=443): Max retries exceeded with url: /providers/POCLOUD/collections/ECCO%20Ocean%20Temperature%20and%20Salinity%20-%20Monthly%20Mean%20llc90%20Grid%20(Version%204%20Release%204)/granules/OCEAN_TEMPERATURE_SALINITY_mon_mean_2017-01_ECCO_V4r4_native_llc0090.dap?dap4.ce=i%5B0%3A1%3A89%5D%3Bj%5B0%3A1%3A89%5D%3Bk%5B0%3A1%3A49%5D%3Btile%5B0%3A1%3A12%5D&dap4.checksum=true (Caused by ResponseError('too many 500 error responses'))

During handling of the above exception, another exception occurred:

RetryError                                Traceback (most recent call last)
Cell In[21], line 1
----> 1 get_ipython().run_cell_magic('time', '', "theta_salt_ds = xr.open_mfdataset(\n    Temp_2017, \n    engine='pydap',\n    session=cache_session, \n    parallel=True, \n    combine='nested', \n    concat_dim='time',\n)\n")

File <timed exec>:1
----> 1 'Could not get source, probably due dynamically evaluated source code.'

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/xarray/backends/api.py:1662, in open_mfdataset(paths, chunks, concat_dim, compat, preprocess, engine, data_vars, coords, combine, parallel, join, attrs_file, combine_attrs, errors, **kwargs)
   1657     datasets = [preprocess(ds) for ds in datasets]
   1659 if parallel:
   1660     # calling compute here will return the datasets/file_objs lists,
   1661     # the underlying datasets will still be stored as dask arrays
-> 1662     datasets, closers = dask.compute(datasets, closers)
   1664 # Combine all datasets, closing them in case of a ValueError
   1665 try:

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/dask/base.py:685, in compute(traverse, optimize_graph, scheduler, get, *args, **kwargs)
    682     expr = expr.optimize()
    683     keys = list(flatten(expr.__dask_keys__()))
--> 685     results = schedule(expr, keys, **kwargs)
    687 return repack(results)

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/xarray/backends/api.py:607, in open_dataset(filename_or_obj, engine, chunks, cache, decode_cf, mask_and_scale, decode_times, decode_timedelta, use_cftime, concat_characters, decode_coords, drop_variables, create_default_indexes, inline_array, chunked_array_type, from_array_kwargs, backend_kwargs, **kwargs)
    595 decoders = _resolve_decoders_kwargs(
    596     decode_cf,
    597     open_backend_dataset_parameters=backend.open_dataset_parameters,
   (...)    603     decode_coords=decode_coords,
    604 )
    606 overwrite_encoded_chunks = kwargs.pop("overwrite_encoded_chunks", None)
--> 607 backend_ds = backend.open_dataset(
    608     filename_or_obj,
    609     drop_variables=drop_variables,
    610     **decoders,
    611     **kwargs,
    612 )
    613 ds = _dataset_from_backend_dataset(
    614     backend_ds,
    615     filename_or_obj,
   (...)    626     **kwargs,
    627 )
    628 return ds

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/xarray/backends/pydap_.py:292, in PydapBackendEntrypoint.open_dataset(self, filename_or_obj, mask_and_scale, decode_times, concat_characters, decode_coords, drop_variables, use_cftime, decode_timedelta, group, application, session, output_grid, timeout, verify, user_charset, checksums)
    290 store_entrypoint = StoreBackendEntrypoint()
    291 with close_on_error(store):
--> 292     ds = store_entrypoint.open_dataset(
    293         store,
    294         mask_and_scale=mask_and_scale,
    295         decode_times=decode_times,
    296         concat_characters=concat_characters,
    297         decode_coords=decode_coords,
    298         drop_variables=drop_variables,
    299         use_cftime=use_cftime,
    300         decode_timedelta=decode_timedelta,
    301     )
    302     return ds

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/xarray/backends/store.py:42, in StoreBackendEntrypoint.open_dataset(self, filename_or_obj, mask_and_scale, decode_times, concat_characters, decode_coords, drop_variables, set_indexes, use_cftime, decode_timedelta)
     27 def open_dataset(
     28     self,
     29     filename_or_obj: T_PathFileOrDataStore,
   (...)     38     decode_timedelta=None,
     39 ) -> Dataset:
     40     assert isinstance(filename_or_obj, AbstractDataStore)
---> 42     vars, attrs = filename_or_obj.load()
     43     encoding = filename_or_obj.get_encoding()
     45     vars, attrs, coord_names = conventions.decode_cf_variables(
     46         vars,
     47         attrs,
   (...)     54         decode_timedelta=decode_timedelta,
     55     )

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/xarray/backends/common.py:370, in AbstractDataStore.load(self)
    351 def load(self):
    352     """
    353     This loads the variables and attributes simultaneously.
    354     A centralized loading function makes it easier to create
   (...)    367     are requested, so care should be taken to make sure its fast.
    368     """
    369     variables = FrozenDict(
--> 370         (_decode_variable_name(k), v) for k, v in self.get_variables().items()
    371     )
    372     attributes = FrozenDict(self.get_attrs())
    373     return variables, attributes

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/xarray/backends/pydap_.py:194, in PydapDataStore.get_variables(self)
    185     from pydap.model import GroupType
    187     _vars = [
    188         var
    189         for var in self.ds.keys()
    190         # check the key is not a BaseType or GridType
    191         if not isinstance(self.ds[var], GroupType)
    192     ]
--> 194 return FrozenDict((k, self.open_store_variable(self.ds[k])) for k in _vars)

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/xarray/core/utils.py:522, in FrozenDict(*args, **kwargs)
    521 def FrozenDict(*args, **kwargs) -> Frozen:
--> 522     return Frozen(dict(*args, **kwargs))

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/xarray/backends/pydap_.py:194, in <genexpr>(.0)
    185     from pydap.model import GroupType
    187     _vars = [
    188         var
    189         for var in self.ds.keys()
    190         # check the key is not a BaseType or GridType
    191         if not isinstance(self.ds[var], GroupType)
    192     ]
--> 194 return FrozenDict((k, self.open_store_variable(self.ds[k])) for k in _vars)

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/xarray/backends/pydap_.py:169, in PydapDataStore.open_store_variable(self, var)
    163 if (
    164     self._protocol == "dap4"
    165     and var.name in dimensions
    166     and hasattr(var, "dataset")  # only True for pydap>3.5.5
    167 ):
    168     var.dataset.enable_batch_mode()
--> 169     data_array = self._get_data_array(var)
    170     data = indexing.LazilyIndexedArray(data_array)
    171     var.dataset.disable_batch_mode()

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/xarray/backends/pydap_.py:228, in PydapDataStore._get_data_array(self, var)
    223 from pydap.client import get_batch_data
    225 if not var._is_data_loaded():
    226     # data has not been deserialized yet
    227     # runs only once per store/hierarchy
--> 228     get_batch_data(var, checksums=self._checksums)
    230 return self.dataset[var.id].data

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/pydap/client.py:1265, in get_batch_data(array, cache_urls, checksums, key)
   1261 try:
   1262     if "consolidated" in ds.dataset.session.headers and set_dims:
   1263         # need to add a check that consolidated has
   1264         # been performed on that collection.
-> 1265         fetch_consolidated(ds, cache_urls=cache_urls, checksums=checksums)
   1266     else:
   1267         dataset = ds.dataset

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/pydap/client.py:1422, in fetch_consolidated(ds, cache_urls, checksums)
   1420 for URL in set(dap_urls):
   1421     try:
-> 1422         r = session.get(URL)
   1423     except (sqlite3.InterfaceError, EOFError):
   1424         with session.cache_disabled():

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/requests_cache/session.py:133, in CacheMixin.get(self, url, params, **kwargs)
    131 def get(self, url: str, params=None, **kwargs) -> AnyResponse:  # type: ignore
    132     kwargs.setdefault('allow_redirects', True)
--> 133     return self.request('GET', url, params=params, **kwargs)

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/requests_cache/session.py:189, in CacheMixin.request(self, method, url, headers, expire_after, only_if_cached, refresh, force_refresh, *args, **kwargs)
    187 headers = set_request_headers(headers, expire_after, only_if_cached, refresh, force_refresh)
    188 with patch_form_boundary() if kwargs.get('files') else nullcontext():
--> 189     return super().request(method, url, *args, headers=headers, **kwargs)

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/requests/sessions.py:651, in Session.request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
    646 send_kwargs = {
    647     "timeout": timeout,
    648     "allow_redirects": allow_redirects,
    649 }
    650 send_kwargs.update(settings)
--> 651 resp = self.send(prep, **send_kwargs)
    653 return resp

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/requests_cache/session.py:255, in CacheMixin.send(self, request, expire_after, only_if_cached, refresh, force_refresh, **kwargs)
    253     response = self._resend(request, actions, cached_response, **kwargs)  # type: ignore
    254 elif actions.send_request:
--> 255     response = self._send_and_cache(request, actions, cached_response, **kwargs)
    256 else:
    257     response = cached_response  # type: ignore  # Guaranteed to be non-None by this point

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/requests_cache/session.py:279, in CacheMixin._send_and_cache(self, request, actions, cached_response, **kwargs)
    275 """Send a request and cache the response, unless disabled by settings or headers.
    276 If applicable, also handle conditional requests.
    277 """
    278 request = actions.update_request(request)
--> 279 response = super().send(request, **kwargs)
    280 actions.update_from_response(response)
    282 if not actions.skip_write:

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/requests/sessions.py:805, in Session.send(self, request, **kwargs)
    802 if allow_redirects:
    803     # Redirect resolving generator.
    804     gen = self.resolve_redirects(r, request, **kwargs)
--> 805     history = [resp for resp in gen]
    806 else:
    807     history = []

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/requests/sessions.py:805, in <listcomp>(.0)
    802 if allow_redirects:
    803     # Redirect resolving generator.
    804     gen = self.resolve_redirects(r, request, **kwargs)
--> 805     history = [resp for resp in gen]
    806 else:
    807     history = []

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/requests/sessions.py:292, in SessionRedirectMixin.resolve_redirects(self, resp, req, stream, timeout, verify, cert, proxies, yield_requests, **adapter_kwargs)
    290     yield req  # type: ignore[misc]  # Internal use only, returns PreparedRequest
    291 else:
--> 292     resp = self.send(
    293         req,
    294         stream=stream,
    295         timeout=timeout,
    296         verify=verify,
    297         cert=cert,
    298         proxies=proxies,
    299         allow_redirects=False,
    300         **adapter_kwargs,
    301     )
    303     extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)
    305     # extract redirect url, if any, for the next loop

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/requests_cache/session.py:255, in CacheMixin.send(self, request, expire_after, only_if_cached, refresh, force_refresh, **kwargs)
    253     response = self._resend(request, actions, cached_response, **kwargs)  # type: ignore
    254 elif actions.send_request:
--> 255     response = self._send_and_cache(request, actions, cached_response, **kwargs)
    256 else:
    257     response = cached_response  # type: ignore  # Guaranteed to be non-None by this point

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/requests_cache/session.py:279, in CacheMixin._send_and_cache(self, request, actions, cached_response, **kwargs)
    275 """Send a request and cache the response, unless disabled by settings or headers.
    276 If applicable, also handle conditional requests.
    277 """
    278 request = actions.update_request(request)
--> 279 response = super().send(request, **kwargs)
    280 actions.update_from_response(response)
    282 if not actions.skip_write:

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/requests/sessions.py:784, in Session.send(self, request, **kwargs)
    781 start = preferred_clock()
    783 # Send the request
--> 784 r = adapter.send(request, **kwargs)
    786 # Total elapsed time of the request (approximately)
    787 elapsed = preferred_clock() - start

File ~/mambaforge/envs/pydap_docs/lib/python3.11/site-packages/requests/adapters.py:720, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    717         raise ConnectTimeout(e, request=request)
    719 if isinstance(e.reason, ResponseError):
--> 720     raise RetryError(e, request=request)
    722 if isinstance(e.reason, _ProxyError):
    723     raise ProxyError(e, request=request)

RetryError: HTTPSConnectionPool(host='opendap.earthdata.nasa.gov', port=443): Max retries exceeded with url: /providers/POCLOUD/collections/ECCO%20Ocean%20Temperature%20and%20Salinity%20-%20Monthly%20Mean%20llc90%20Grid%20(Version%204%20Release%204)/granules/OCEAN_TEMPERATURE_SALINITY_mon_mean_2017-01_ECCO_V4r4_native_llc0090.dap?dap4.ce=i%5B0%3A1%3A89%5D%3Bj%5B0%3A1%3A89%5D%3Bk%5B0%3A1%3A49%5D%3Btile%5B0%3A1%3A12%5D&dap4.checksum=true (Caused by ResponseError('too many 500 error responses'))
theta_salt_ds

Finalmente, visualizamos THETA#

Variable = [theta_salt_ds['THETA'][0, 0, i, :, :] for i in range(13)]
clevels = np.linspace(-5, 30, 100)
cMap='RdBu_r'
fig, axes = plt.subplots(nrows=4, ncols=4, figsize=(8, 8), gridspec_kw={'hspace':0.01, 'wspace':0.01})
AXES_NR = [
    axes[3, 0], axes[2, 0], axes[1, 0], axes[3, 1], axes[2, 1], axes[1, 1],
]
AXES_CAP = [axes[0, 0]]
AXES_R = [
    axes[1, 2], axes[2, 2], axes[3, 2], axes[1, 3], axes[2, 3], axes[3, 3],
]
for i in range(len(AXES_NR)):
    AXES_NR[i].contourf(Variable[i], clevels, cmap=cMap)

for i in range(len(AXES_CAP)):
    AXES_CAP[i].contourf(Variable[6].transpose()[:, ::-1], clevels, cmap=cMap)

for i in range(len(AXES_R)):
    AXES_R[i].contourf(Variable[7+i].transpose()[::-1, :], clevels, cmap=cMap)

for ax in np.ravel(axes):
    ax.axis('off')
    plt.setp(ax.get_xticklabels(), visible=False)
    plt.setp(ax.get_yticklabels(), visible=False)

plt.show()

Fig. 3. Visualizacion de la variable Surface temperature, similar al metodo utilizado en la Figura 2.