Misleading error messages in the plotting functions

I've been having some problems with the plot function in pyicon, with the error messages being given not really pointing to the problem. I've detailed some of these errors below. I'll submit a pull request to try and improve the error messages later this week.

Producing the errors

Problem 1

import xarray as xr
import pyicon as pyic

ds = xr.open_mfdataset(
    "/home/m/m301014/work.me/exp.TropAtl/data/raw/exp.TropAtl.r2b6/exp.TropAtl.r2b6_production_1955????T000000Z.nc",
    decode_cf=False,
)

ds["vort"].isel(time=10, depth=0).pyic.plot()
File /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/pyicon/pyicon_accessor.py:26, in pyiconDataArray.plot(self, **kwargs)
23 @functools.wraps(pyic.plot, assigned=("__doc__", "__anotations__"))
24 def plot(self, **kwargs):
25     da = self._obj
---> 26     ax, hm = pyic.plot(da, **kwargs)
27     return ax, hmFile /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/pyicon/pyicon_plotting.py:1966, in plot(data, Plot, ax, cax, asp, fig_size_fac, mask_data, logplot, lon_reg, lat_reg, central_longitude, clim, cmap, conts, contfs, clevs, use_pcol_or_contf, contcolor, cincr, clabel, cbticks, xlabel, ylabel, xticks, yticks, template, cbar_str, cbar_pos, title_right, title_left, title_center, projection, coastlines_color, land_facecolor, axes_facecolor, noland, do_plot_settings, do_xyticks, do_gridlines, gname, fpath_tgrid, plot_method, grid_type, res, fpath_ckdtree, coordinates, lonlat_for_mask)
1963   fpath_ckdtree = f'{path_grid}/{gname}/ckdtree/rectgrids/{gname}_res{res:3.2f}_180W-180E_90S-90N.nc'
1965 if grid_type=='auto':
-> 1966   if Dgrid["name"].startswith("healpix"):
1967     grid_type = 'healpix'
1968   else:UnboundLocalError: cannot access local variable 'Dgrid' where it is not associated with a value

This is fixed by including grid_type="native" in the function call. But then we get a new error....

Problem 2

ds["vort"].isel(time=10, depth=0).pyic.plot(
    grid_type="native",
)
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/xarray/backends/file_manager.py:211, in CachingFileManager._acquire_with_cache_info(self, needs_lock)
210 try:
--> 211     file = self._cache[self._key]
212 except KeyError:File /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/xarray/backends/lru_cache.py:56, in LRUCache.__getitem__(self, key)
55 with self._lock:
---> 56     value = self._cache[key]
57     self._cache.move_to_end(key)KeyError: [<class 'netCDF4._netCDF4.Dataset'>, ('/work/mh0033/m300602/icon/grids/none/ckdtree/rectgrids/none_res0.30_180W-180E_90S-90N.nc',), 'r', (('clobber', True), ('diskless', False), ('format', 'NETCDF4'), ('persist', False)), 'ab78d15b-6e6b-4165-a718-d702fcc4afe4']During handling of the above exception, another exception occurred:FileNotFoundError                         Traceback (most recent call last)
Cell In[242], line 1
----> 1 ds["vort"].isel(time=10, depth=0).pyic.plot(
2     grid_type="native"
3 )File /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/pyicon/pyicon_accessor.py:26, in pyiconDataArray.plot(self, **kwargs)
23 @functools.wraps(pyic.plot, assigned=("__doc__", "__anotations__"))
24 def plot(self, **kwargs):
25     da = self._obj
---> 26     ax, hm = pyic.plot(da, **kwargs)
27     return ax, hmFile /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/pyicon/pyicon_plotting.py:1994, in plot(data, Plot, ax, cax, asp, fig_size_fac, mask_data, logplot, lon_reg, lat_reg, central_longitude, clim, cmap, conts, contfs, clevs, use_pcol_or_contf, contcolor, cincr, clabel, cbticks, xlabel, ylabel, xticks, yticks, template, cbar_str, cbar_pos, title_right, title_left, title_center, projection, coastlines_color, land_facecolor, axes_facecolor, noland, do_plot_settings, do_xyticks, do_gridlines, gname, fpath_tgrid, plot_method, grid_type, res, fpath_ckdtree, coordinates, lonlat_for_mask)
1989 #if 'grid_mapping' in list(data.attrs):
1990 #  plot_method = 'healpix'
1991
1992 # --- interpolate and cut to region
1993 if grid_type=='native' and plot_method=='nn':
-> 1994   datai = interp_to_rectgrid_xr(data.compute(), fpath_ckdtree, lon_reg=lon_reg, lat_reg=lat_reg, coordinates=coordinates)
1995   lon = datai.lon
1996   lat = datai.latFile /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/pyicon/pyicon_tb.py:241, in interp_to_rectgrid_xr(arr, fpath_ckdtree, lon_reg, lat_reg, coordinates, radius_of_influence, compute, mask_out_of_range, mask_out_of_range_before)
238   print(fpath_ckdtree)
240 # --- load interpolation indices
--> 241 ds_ckdt = xr.open_dataset(fpath_ckdtree)
242 if ('clon' in coordinates) or (coordinates==''):
243   inds = ds_ckdt.ickdtree_cFile /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/xarray/backends/api.py:572, 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, inline_array, chunked_array_type, from_array_kwargs, backend_kwargs, **kwargs)
560 decoders = _resolve_decoders_kwargs(
561     decode_cf,
562     open_backend_dataset_parameters=backend.open_dataset_parameters,
(...)
568     decode_coords=decode_coords,
569 )
571 overwrite_encoded_chunks = kwargs.pop("overwrite_encoded_chunks", None)
--> 572 backend_ds = backend.open_dataset(
573     filename_or_obj,
574     drop_variables=drop_variables,
575     **decoders,
576     **kwargs,
577 )
578 ds = _dataset_from_backend_dataset(
579     backend_ds,
580     filename_or_obj,
(...)
590     **kwargs,
591 )
592 return dsFile /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/xarray/backends/netCDF4_.py:593, in NetCDF4BackendEntrypoint.open_dataset(self, filename_or_obj, mask_and_scale, decode_times, concat_characters, decode_coords, drop_variables, use_cftime, decode_timedelta, group, mode, format, clobber, diskless, persist, lock, autoclose)
572 def open_dataset(  # type: ignore[override]  # allow LSP violation, not supporting **kwargs
573     self,
574     filename_or_obj: str | os.PathLike[Any] | BufferedIOBase | AbstractDataStore,
(...)
590     autoclose=False,
591 ) -> Dataset:
592     filename_or_obj = _normalize_path(filename_or_obj)
--> 593     store = NetCDF4DataStore.open(
594         filename_or_obj,
595         mode=mode,
596         format=format,
597         group=group,
598         clobber=clobber,
599         diskless=diskless,
600         persist=persist,
601         lock=lock,
602         autoclose=autoclose,
603     )
605     store_entrypoint = StoreBackendEntrypoint()
606     with close_on_error(store):File /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/xarray/backends/netCDF4_.py:400, in NetCDF4DataStore.open(cls, filename, mode, format, group, clobber, diskless, persist, lock, lock_maker, autoclose)
394 kwargs = dict(
395     clobber=clobber, diskless=diskless, persist=persist, format=format
396 )
397 manager = CachingFileManager(
398     netCDF4.Dataset, filename, mode=mode, kwargs=kwargs
399 )
--> 400 return cls(manager, group=group, mode=mode, lock=lock, autoclose=autoclose)File /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/xarray/backends/netCDF4_.py:347, in NetCDF4DataStore.__init__(self, manager, group, mode, lock, autoclose)
345 self._group = group
346 self._mode = mode
--> 347 self.format = self.ds.data_model
348 self._filename = self.ds.filepath()
349 self.is_remote = is_remote_uri(self._filename)File /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/xarray/backends/netCDF4_.py:409, in NetCDF4DataStore.ds(self)
407 @property
408 def ds(self):
--> 409     return self._acquire()File /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/xarray/backends/netCDF4_.py:403, in NetCDF4DataStore._acquire(self, needs_lock)
402 def _acquire(self, needs_lock=True):
--> 403     with self._manager.acquire_context(needs_lock) as root:
404         ds = _nc4_require_group(root, self._group, self._mode)
405     return dsFile /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/contextlib.py:137, in _GeneratorContextManager.__enter__(self)
135 del self.args, self.kwds, self.func
136 try:
--> 137     return next(self.gen)
138 except StopIteration:
139     raise RuntimeError("generator didn't yield") from NoneFile /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/xarray/backends/file_manager.py:199, in CachingFileManager.acquire_context(self, needs_lock)
196 @contextlib.contextmanager
197 def acquire_context(self, needs_lock=True):
198     """Context manager for acquiring a file."""
--> 199     file, cached = self._acquire_with_cache_info(needs_lock)
200     try:
201         yield fileFile /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/xarray/backends/file_manager.py:217, in CachingFileManager._acquire_with_cache_info(self, needs_lock)
215     kwargs = kwargs.copy()
216     kwargs["mode"] = self._mode
--> 217 file = self._opener(*self._args, **kwargs)
218 if self._mode == "w":
219     # ensure file doesn't get overridden when opened again
220     self._mode = "a"File src/netCDF4/_netCDF4.pyx:2469, in netCDF4._netCDF4.Dataset.__init__()File src/netCDF4/_netCDF4.pyx:2028, in netCDF4._netCDF4._ensure_nc_success()FileNotFoundError: [Errno 2] No such file or directory: '/work/mh0033/m300602/icon/grids/none/ckdtree/rectgrids/none_res0.30_180W-180E_90S-90N.nc'

This is fixed by gname="r2b6_oce_r0004" but leads to problem 3... I think we can fix this by raising an error when gname is set to None.

Problem 3

ds["vort"].isel(time=10, depth=0).pyic.plot(
grid_type="native",
gname="r2b6_oce_r0004",
)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[243], line 1
----> 1 ds["vort"].isel(time=10, depth=0).pyic.plot(
2     grid_type="native",
3     gname="r2b6_oce_r0004",
4 )File /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/pyicon/pyicon_accessor.py:26, in pyiconDataArray.plot(self, **kwargs)
23 @functools.wraps(pyic.plot, assigned=("__doc__", "__anotations__"))
24 def plot(self, **kwargs):
25     da = self._obj
---> 26     ax, hm = pyic.plot(da, **kwargs)
27     return ax, hmFile /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/pyicon/pyicon_plotting.py:1994, in plot(data, Plot, ax, cax, asp, fig_size_fac, mask_data, logplot, lon_reg, lat_reg, central_longitude, clim, cmap, conts, contfs, clevs, use_pcol_or_contf, contcolor, cincr, clabel, cbticks, xlabel, ylabel, xticks, yticks, template, cbar_str, cbar_pos, title_right, title_left, title_center, projection, coastlines_color, land_facecolor, axes_facecolor, noland, do_plot_settings, do_xyticks, do_gridlines, gname, fpath_tgrid, plot_method, grid_type, res, fpath_ckdtree, coordinates, lonlat_for_mask)
1989 #if 'grid_mapping' in list(data.attrs):
1990 #  plot_method = 'healpix'
1991
1992 # --- interpolate and cut to region
1993 if grid_type=='native' and plot_method=='nn':
-> 1994   datai = interp_to_rectgrid_xr(data.compute(), fpath_ckdtree, lon_reg=lon_reg, lat_reg=lat_reg, coordinates=coordinates)
1995   lon = datai.lon
1996   lat = datai.latFile /work/mh0256/m301014/.conda/envs/age-tracer/lib/python3.11/site-packages/pyicon/pyicon_tb.py:285, in interp_to_rectgrid_xr(arr, fpath_ckdtree, lon_reg, lat_reg, coordinates, radius_of_influence, compute, mask_out_of_range, mask_out_of_range_before)
283 # --- interpolate by nearest neighbor
284 inds = inds.flatten()
--> 285 arr_interp = xr.DataArray(arr.data[inds].reshape(lat.size, lon.size), dims=['lat', 'lon'], coords=dict(lat=lat, lon=lon))
287 # --- mask values where nearest neighbor is too far away
288 # (doing this after compute seems to be faster) FIXME check that!
289 if mask_out_of_range_before:IndexError: index 203212 is out of bounds for axis 0 with size 120947

This is fixed by coordinates="vlon vlat" and I suspect we can get pyicon to automatically guess how to fix this type of problem.

So the final working piece of code is

ds["vort"].isel(time=10, depth=0).pyic.plot(
grid_type="native",
gname="r2b6_oce_r0004",
coordinates="vlon vlat",
)