Prototype of bounded dataarray functionality
Description
This PR is meant to prototype and explore avenues for enabling xcdat functionality to operate on dataarray objects. The initial PR takes the approach of a) embedding bounds in a "bounded dataarray" (following this xarray exchange), b) converting the bounded dataarray to a dataset to call xcdat functionality (spatial averaging was prototyped).
This approach seems to work fine and is fast (converting between datasets/dataarrays is extremely fast), but it does require some manual re-implementation of functions and documentation. It also is only currently prototyped for rectilinear grids (but maybe a similar approach could be followed for other grid types). Last, it overwrites xr.Dataset.__call__ with a function that converts the dataset to a bounded dataarray, which might be inadvisable (we could avoid this if we needed to, by calling the underlying function directly, e.g., tas = xc.to_bounded_dataarray(ds, "tas")).
Alternate approached could be explored (in this PR or separate PRs):
- The "bounded datarray" is actually just a dataarray, but maybe we should actually create a new class
- Re-factor xcdat logic to be able to work with dataset bounds or dataarray bounds (the challenge here is that accessors are specific to dataarrays and datasets)
- Re-implement all existing logic on dataarrays (rather than flipping back and forth between dataarray/dataset objects) [nonstarter?]
- Some other creative solution?
Regardless, it might be useful to attend an xarray meeting to discuss what they think makes sense.
- Closes #725
Checklist
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings
- [ ] Any dependent changes have been merged and published in downstream modules
If applicable:
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass with my changes (locally and CI/CD build)
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] I have noted that this is a breaking change for a major release (fix or feature that would cause existing functionality to not work as expected)
Another thing to note is that Xarray does not have a native API for generating bounds. Pitching our bounds generation functionalities might also be a good idea, considering I think they're generic enough (except for time frequency based on CF calendar).
@tomvothecoder – I asked ChatGPT if there was a better way to approach this problem and it yielded a slightly different approach for including the bounds in the dataarray:
import xarray as xr
import xcdat as xc
@xr.register_dataarray_accessor("cf_bounds")
class CFBoundsAccessor:
def __init__(self, xarray_obj):
self._obj = xarray_obj
# Initialize or load bounds as a dictionary from attributes.
self._bounds = self._obj.attrs.get("bounds", {})
@property
def bounds(self):
"""Return the entire dictionary of bounds."""
if not self._bounds:
raise AttributeError("No bounds are attached to this DataArray.")
return self._bounds
def set_bounds(self, bounds_dict):
"""
Set multiple bounds at once.
Parameters
----------
bounds_dict : dict
A dictionary mapping names (e.g., 'lat_bnds', 'lon_bnds', 'time_bnds')
to their respective bounds arrays.
"""
if not isinstance(bounds_dict, dict):
raise TypeError("bounds_dict must be a dictionary.")
self._bounds = bounds_dict
self._obj.attrs["bounds"] = bounds_dict
def get_bounds(self, key):
"""
Retrieve the bounds for a given coordinate or bounds name.
Parameters
----------
key : str
The key corresponding to the bounds you want to retrieve.
Returns
-------
The bounds array associated with the given key.
"""
try:
return self._bounds[key]
except KeyError:
raise KeyError(f"No bounds available for key '{key}'.")
def add_bound(self, key, bound_array):
"""
Add or update a single bounds entry.
Parameters
----------
key : str
The key for the bounds (e.g., 'lat_bnds').
bound_array : array-like
The bounds array corresponding to the key.
"""
self._bounds[key] = bound_array
self._obj.attrs["bounds"] = self._bounds
ds = xc.open_dataset('tas_Amon_MIROC-ES2L_historical_r2i1p1f2_gn_185001-201412.nc')
tas = ds['tas']
tas.cf_bounds.set_bounds({
"lat_bnds": ds.lat_bnds,
"lon_bnds": ds.lon_bnds
})
print(tas.cf_bounds.get_bounds("lat_bnds"))
print(tas.cf_bounds.get_bounds("lon_bnds"))
<xarray.DataArray 'lat_bnds' (lat: 64, bnds: 2)> Size: 1kB
[128 values with dtype=float64] Coordinates:
- lat (lat) float64 512B -87.86 -85.1 -82.31 -79.53 ... 82.31 85.1 87.86 height float64 8B 2.0 Dimensions without coordinates: bnds <xarray.DataArray 'lon_bnds' (lon: 128, bnds: 2)> Size: 2kB [256 values with dtype=float64] Coordinates:
- lon (lon) float64 1kB 0.0 2.812 5.625 8.438 ... 348.8 351.6 354.4 357.2 height float64 8B 2.0 Dimensions without coordinates: bnds
I like this approach (generally; I'd change some thing) because it seems to store the bounds in their native dataarray form, but thinking back (5 years ago?) – did you try something like this?
I also asked ChatGPT: once we have a dataarray with bounds included, how can we best leverage our existing functions to perform operations (e.g., spatial averaging). It basically came back with:
- write functions that can handle either dataset or dataarrays
- the approach in this PR (convert the dataarray back to a dataset, call dataset accessor functions, return dataarray result)
@tomvothecoder – I asked ChatGPT if there was a better way to approach this problem and it yielded a slightly different approach for including the bounds in the
dataarray:import xarray as xr import xcdat as xc @xr.register_dataarray_accessor("cf_bounds") class CFBoundsAccessor: def __init__(self, xarray_obj): self._obj = xarray_obj # Initialize or load bounds as a dictionary from attributes. self._bounds = self._obj.attrs.get("bounds", {}) @property def bounds(self): """Return the entire dictionary of bounds.""" if not self._bounds: raise AttributeError("No bounds are attached to this DataArray.") return self._bounds def set_bounds(self, bounds_dict): """ Set multiple bounds at once. Parameters ---------- bounds_dict : dict A dictionary mapping names (e.g., 'lat_bnds', 'lon_bnds', 'time_bnds') to their respective bounds arrays. """ if not isinstance(bounds_dict, dict): raise TypeError("bounds_dict must be a dictionary.") self._bounds = bounds_dict self._obj.attrs["bounds"] = bounds_dict def get_bounds(self, key): """ Retrieve the bounds for a given coordinate or bounds name. Parameters ---------- key : str The key corresponding to the bounds you want to retrieve. Returns ------- The bounds array associated with the given key. """ try: return self._bounds[key] except KeyError: raise KeyError(f"No bounds available for key '{key}'.") def add_bound(self, key, bound_array): """ Add or update a single bounds entry. Parameters ---------- key : str The key for the bounds (e.g., 'lat_bnds'). bound_array : array-like The bounds array corresponding to the key. """ self._bounds[key] = bound_array self._obj.attrs["bounds"] = self._bounds ds = xc.open_dataset('tas_Amon_MIROC-ES2L_historical_r2i1p1f2_gn_185001-201412.nc') tas = ds['tas'] tas.cf_bounds.set_bounds({ "lat_bnds": ds.lat_bnds, "lon_bnds": ds.lon_bnds }) print(tas.cf_bounds.get_bounds("lat_bnds")) print(tas.cf_bounds.get_bounds("lon_bnds"))<xarray.DataArray 'lat_bnds' (lat: 64, bnds: 2)> Size: 1kB [128 values with dtype=float64] Coordinates:
- lat (lat) float64 512B -87.86 -85.1 -82.31 -79.53 ... 82.31 85.1 87.86 height float64 8B 2.0 Dimensions without coordinates: bnds <xarray.DataArray 'lon_bnds' (lon: 128, bnds: 2)> Size: 2kB [256 values with dtype=float64] Coordinates:
- lon (lon) float64 1kB 0.0 2.812 5.625 8.438 ... 348.8 351.6 354.4 357.2 height float64 8B 2.0 Dimensions without coordinates: bnds
I like this approach (generally; I'd change some thing) because it seems to store the bounds in their native
dataarrayform, but thinking back (5 years ago?) – did you try something like this?I also asked ChatGPT: once we have a dataarray with bounds included, how can we best leverage our existing functions to perform operations (e.g., spatial averaging). It basically came back with:
* write functions that can handle either dataset or dataarrays * the approach in this PR (convert the dataarray back to a dataset, call dataset accessor functions, return dataarray result)
I tried a similar approach where I created a datarray accessor class and stored bounds as an attribute. The issue was that accessor classes are not persistent when creating new instances of the dataarray (e.g., copying the object). This means the accessor class attributes such as _bounds get wiped.
If possible, we need to figure out how to make accessor classes persistent. I prefer this approach over sub-classing/overwriting Xarray API methods since it seems cleaner and less intrusive.
I'll try experimenting more before our meeting on Wed.
I tried a similar approach where I created a datarray accessor class and stored bounds as an attribute. The issue was that accessor classes are not persistent when creating new instances of the dataarray (e.g., copying the object). This means the accessor class attributes such as _bounds get wiped.
This is what I remember. I did try the code above and when I do the following:
x = tas.copy()
del tas
print(x.cf_bounds.get_bounds("lat_bnds"))
print(x.cf_bounds.get_bounds("lon_bnds"))
I still see the bounds in the copied object.
I tried a similar approach where I created a datarray accessor class and stored bounds as an attribute. The issue was that accessor classes are not persistent when creating new instances of the dataarray (e.g., copying the object). This means the accessor class attributes such as _bounds get wiped.
This is what I remember. I did try the code above and when I do the following:
x = tas.copy() del tas print(x.cf_bounds.get_bounds("lat_bnds")) print(x.cf_bounds.get_bounds("lon_bnds"))I still see the bounds in the copied object.
How about when you copy x? xCDAT APIs copy objects underneath the hood, which means chaining different operations like regridding and spatial averaging will probably result in dropped accessor class attributes.
bounds sticks around when I copy x, though I can't call xcdat accessors (because x is a DataArray) to see if they drop with xcdat operations...
boundssticks around when I copyx, though I can't callxcdataccessors (becausexis a DataArray) to see if they drop with xcdat operations...
Oh I see what you're doing with the accessor class. You're saving the bounds as a dictionary under .attrs, which is persistent. The CFBoundsAccessor simply sets the its self._bounds attribute by grabbing that dict entry. I like this approach.
One prominent issue is that attempting to write the DataArray will result in For serialization to netCDF files, its value must be of one of the following types: str, Number, ndarray, number, list, tuple, bytes for .attrs.
TypeError Traceback (most recent call last)
File /home/vo13/xCDAT/xcdat/qa/737-boundedarray/qa.py:3
[1](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/qa/737-boundedarray/qa.py:1) # %%
[2](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/qa/737-boundedarray/qa.py:2) # Write the DataArray to a netCDF file
----> [3](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/qa/737-boundedarray/qa.py:3) x.to_netcdf("/home/vo13/xCDAT/xcdat/qa/737-boundedarray/output.nc")
[5](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/qa/737-boundedarray/qa.py:5) # Read the DataArray back from the netCDF file
[6](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/qa/737-boundedarray/qa.py:6) x_loaded = xr.open_dataarray("/home/vo13/xCDAT/xcdat/qa/737-boundedarray/output.nc")
File ~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4204, in DataArray.to_netcdf(self, path, mode, format, group, engine, encoding, unlimited_dims, compute, invalid_netcdf, auto_complex)
[4200](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4200) else:
[4201](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4201) # No problems with the name - so we're fine!
[4202](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4202) dataset = self.to_dataset()
-> [4204](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4204) return to_netcdf( # type: ignore[return-value] # mypy cannot resolve the overloads:(
[4205](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4205) dataset,
[4206](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4206) path,
[4207](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4207) mode=mode,
[4208](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4208) format=format,
[4209](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4209) group=group,
[4210](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4210) engine=engine,
[4211](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4211) encoding=encoding,
[4212](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4212) unlimited_dims=unlimited_dims,
[4213](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4213) compute=compute,
[4214](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4214) multifile=False,
[4215](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4215) invalid_netcdf=invalid_netcdf,
[4216](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4216) auto_complex=auto_complex,
[4217](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/core/dataarray.py:4217) )
File ~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:1878, in to_netcdf(dataset, path_or_file, mode, format, group, engine, encoding, unlimited_dims, compute, multifile, invalid_netcdf, auto_complex)
[1876](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:1876) # validate Dataset keys, DataArray names, and attr keys/values
[1877](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:1877) _validate_dataset_names(dataset)
-> [1878](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:1878) _validate_attrs(dataset, engine, invalid_netcdf)
[1880](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:1880) try:
[1881](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:1881) store_open = WRITEABLE_STORES[engine]
File ~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:232, in _validate_attrs(dataset, engine, invalid_netcdf)
[230](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:230) for variable in dataset.variables.values():
[231](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:231) for k, v in variable.attrs.items():
--> [232](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:232) check_attr(k, v, valid_types)
File ~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:202, in _validate_attrs.<locals>.check_attr(name, value, valid_types)
[196](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:196) raise TypeError(
[197](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:197) f"Invalid name for attr: {name!r} must be a string for "
[198](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:198) "serialization to netCDF files"
[199](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:199) )
[201](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:201) if not isinstance(value, valid_types):
--> [202](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:202) raise TypeError(
[203](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:203) f"Invalid value for attr {name!r}: {value!r}. For serialization to "
[204](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:204) "netCDF files, its value must be of one of the following types: "
[205](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:205) f"{', '.join([vtype.__name__ for vtype in valid_types])}"
[206](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:206) )
[208](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:208) if isinstance(value, bytes) and engine == "h5netcdf":
[209](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_080/lib/python3.12/site-packages/xarray/backends/api.py:209) try:
TypeError: Invalid value for attr 'bounds': {'lat_bnds': <xarray.DataArray 'lat_bnds' (lat: 10, bnds: 2)> Size: 160B
array([[-100., -80.],
[ -80., -60.],
[ -60., -40.],
[ -40., -20.],
[ -20., 0.],
[ 0., 20.],
[ 20., 40.],
[ 40., 60.],
[ 60., 80.],
[ 80., 100.]])
Coordinates:
* lat (lat) float64 80B -90.0 -70.0 -50.0 -30.0 ... 30.0 50.0 70.0 90.0
Dimensions without coordinates: bnds, 'lon_bnds': <xarray.DataArray 'lon_bnds' (lon: 20, bnds: 2)> Size: 320B
array([[-1.89473684e+02, -1.70526316e+02],
[-1.70526316e+02, -1.51578947e+02],
[-1.51578947e+02, -1.32631579e+02],
[-1.32631579e+02, -1.13684211e+02],
[-1.13684211e+02, -9.47368421e+01],
[-9.47368421e+01, -7.57894737e+01],
[-7.57894737e+01, -5.68421053e+01],
[-5.68421053e+01, -3.78947368e+01],
[-3.78947368e+01, -1.89473684e+01],
[-1.89473684e+01, -1.42108547e-14],
[-1.42108547e-14, 1.89473684e+01],
[ 1.89473684e+01, 3.78947368e+01],
[ 3.78947368e+01, 5.68421053e+01],
[ 5.68421053e+01, 7.57894737e+01],
[ 7.57894737e+01, 9.47368421e+01],
[ 9.47368421e+01, 1.13684211e+02],
[ 1.13684211e+02, 1.32631579e+02],
[ 1.32631579e+02, 1.51578947e+02],
[ 1.51578947e+02, 1.70526316e+02],
[ 1.70526316e+02, 1.89473684e+02]])
Coordinates:
* lon (lon) float64 160B -180.0 -161.1 -142.1 ... 142.1 161.1 180.0
Dimensions without coordinates: bnds}. For serialization to netCDF files, its value must be of one of the following types: str, Number, ndarray, number, list, tuple, bytes
I revived the conversation in the Xarray GitHub issue here: https://github.com/pydata/xarray/issues/1475#issuecomment-2716063411
FYI: https://github.com/pydata/xarray/pull/10137 was just merged, which enables DataArrays to hold coordinate bounds. Xarray v2025.04.0 should include this feature.
Allow assigning index coordinates with non-array dimension(s) in a DataArray by overriding Index.should_add_coord_to_array(). For example, this enables support for CF boundaries coordinate (e.g., time(time) and time_bnds(time, nbnd)) in a DataArray (PR10137). By Benoit Bovy. -- https://docs.xarray.dev/en/latest/whats-new.html#new-features
That's awesome. Should I update this PR to try to include that advance?
That's awesome. Should I update this PR to try to include that advance?
Sure, you can try it out in this PR. Here's the link to one of my proposed API suggestions to support DataArrays in xCDAT, which we discussed in a meeting: https://github.com/xCDAT/xcdat/issues/725#issuecomment-2761944445
Note, this Xarray feature is on main but not v2025.04.0. I think it'll be out on v2025.05.0.