Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: Getting data CF-compliant for regridding (with xesmf) #718

Open
pochedls opened this issue Dec 20, 2024 · 20 comments · May be fixed by #736
Open

[Bug]: Getting data CF-compliant for regridding (with xesmf) #718

pochedls opened this issue Dec 20, 2024 · 20 comments · May be fixed by #736
Assignees
Labels
type: bug Inconsistencies or issues which will cause an issue or problem for users or implementors.

Comments

@pochedls
Copy link
Collaborator

pochedls commented Dec 20, 2024

What happened?

In regridding some ocean data, many (not all) datasets are returning:

ValueError: dataset must include lon/lat or be CF-compliant

I'm having trouble getting the datasets to be CF-compliant on xcdat 0.6.1 / 0.7.1 / 0.7.3. I think xcdat used to better handle ocean grids (though I'm not sure when this might have changed).

What did you expect to happen? Are there are possible answers you came across?

Ideally xcdat could figure out the input grid automatically. If not, this issue is still useful for sorting out what we need to specify in the metadata to make the dataset CF-compliant for xesmf.

Minimal Complete Verifiable Example (MVCE)

# imports
import xcdat as xc
import numpy as np

# target grid
nlat = xc.create_axis('lat', np.arange(-88.5, 90, 2.5))
nlon = xc.create_axis('lon', np.arange(1.25, 360, 2.5))
ngrid = xc.create_grid(x=nlon, y=nlat)

# open / regrid
p = '/p/css03/esgf_publish/CMIP6/ScenarioMIP/NCAR/CESM2-WACCM/ssp585/r1i1p1f1/Omon/fgco2/gn/v20200702/'
ds = xc.open_mfdataset(p)
ds = ds.regridder.horizontal('fgco2', ngrid, tool='xesmf', method='conservative_normed', periodic=True)

Relevant log output

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File ~/bin/anaconda3/envs/xcdat/lib/python3.12/site-packages/xesmf/frontend.py:75, in _get_lon_lat(ds)
     74 try:
---> 75     lon = ds.cf['longitude']
     76     lat = ds.cf['latitude']

File ~/bin/anaconda3/envs/xcdat/lib/python3.12/site-packages/cf_xarray/accessor.py:2343, in CFDatasetAccessor.__getitem__(self, key)
   2312 """
   2313 Index into a Dataset making use of CF attributes.
   2314
   (...)
   2341 Add additional keys by specifying "custom criteria". See :ref:`custom_criteria` for more.
   2342 """
-> 2343 return _getitem(self, key)

File ~/bin/anaconda3/envs/xcdat/lib/python3.12/site-packages/cf_xarray/accessor.py:936, in _getitem(accessor, key, skip)
    935 except KeyError:
--> 936     raise KeyError(
    937         f"{kind}.cf does not understand the key {k!r}. "
    938         f"Use 'repr({kind}.cf)' (or '{kind}.cf' in a Jupyter environment) to see a list of key names that can be interpreted."
    939     ) from None

KeyError: "Dataset.cf does not understand the key 'longitude'. Use 'repr(Dataset.cf)' (or 'Dataset.cf' in a Jupyter environment) to see a list of key names that can be interpreted."

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
Cell In[1], line 13
     11 p = '/p/css03/esgf_publish/CMIP6/ScenarioMIP/NCAR/CESM2-WACCM/ssp585/r1i1p1f1/Omon/fgco2/gn/v20200702/'
     12 ds = xc.open_mfdataset(p)
---> 13 ds = ds.regridder.horizontal('fgco2', ngrid, tool='xesmf', method='conservative_normed', periodic=True)

File ~/bin/anaconda3/envs/xcdat/lib/python3.12/site-packages/xcdat/regridder/accessor.py:205, in RegridderAccessor.horizontal(self, data_var, output_grid, tool, **options)
    203 input_grid = _get_input_grid(self._ds, data_var, ["X", "Y"])
    204 regridder = regrid_tool(input_grid, output_grid, **options)
--> 205 output_ds = regridder.horizontal(data_var, self._ds)
    207 return output_ds

File ~/bin/anaconda3/envs/xcdat/lib/python3.12/site-packages/xcdat/regridder/xesmf.py:158, in XESMFRegridder.horizontal(self, data_var, ds)
    153 if input_da is None:
    154     raise KeyError(
    155         f"The data variable '{data_var}' does not exist in the dataset."
    156     )
--> 158 regridder = xe.Regridder(
    159     self._input_grid,
    160     self._output_grid,
    161     method=self._method,
    162     **self._extra_options,
    163 )
    165 output_da = regridder(input_da, keep_attrs=True)
    167 output_ds = xr.Dataset({data_var: output_da}, attrs=ds.attrs)

File ~/bin/anaconda3/envs/xcdat/lib/python3.12/site-packages/xesmf/frontend.py:919, in Regridder.__init__(self, ds_in, ds_out, method, locstream_in, locstream_out, periodic, parallel, **kwargs)
    917     grid_in, shape_in, input_dims = ds_to_ESMFlocstream(ds_in)
    918 else:
--> 919     grid_in, shape_in, input_dims = ds_to_ESMFgrid(
    920         ds_in, need_bounds=need_bounds, periodic=periodic
    921     )
    922 if locstream_out:
    923     grid_out, shape_out, output_dims = ds_to_ESMFlocstream(ds_out)

File ~/bin/anaconda3/envs/xcdat/lib/python3.12/site-packages/xesmf/frontend.py:145, in ds_to_ESMFgrid(ds, need_bounds, periodic, append)
    115 """
    116 Convert xarray DataSet or dictionary to ESMF.Grid object.
    117
   (...)
    141
    142 """
    143 # use np.asarray(dr) instead of dr.values, so it also works for dictionary
--> 145 lon, lat = _get_lon_lat(ds)
    146 if hasattr(lon, 'dims'):
    147     if lon.ndim == 1:

File ~/bin/anaconda3/envs/xcdat/lib/python3.12/site-packages/xesmf/frontend.py:80, in _get_lon_lat(ds)
     76     lat = ds.cf['latitude']
     77 except (KeyError, AttributeError, ValueError):
     78     # KeyError if cfxr doesn't detect the coords
     79     # AttributeError if ds is a dict
---> 80     raise ValueError('dataset must include lon/lat or be CF-compliant')
     82 return lon, lat

ValueError: dataset must include lon/lat or be CF-compliant

Anything else we need to know?

Note the same code works with p = '/p/css03/esgf_publish/CMIP6/ScenarioMIP/BCC/BCC-CSM2-MR/ssp585/r1i1p1f1/Omon/fgco2/gn/v20190319/' (though I now think this may be because the grid is rectilinear).

Environment

xcdat 0.7.3
xesmf 0.8.8

@pochedls pochedls added the type: bug Inconsistencies or issues which will cause an issue or problem for users or implementors. label Dec 20, 2024
@pochedls
Copy link
Collaborator Author

I see in xesmf docs that:

periodic : bool, optional
Periodic in longitude? Default to False.
Only useful for global grids with non-conservative regridding.
Will be forced to False for conservative regridding.

Updating this to False doesn't fix the issue.

@pochedls
Copy link
Collaborator Author

The following seems to be working as a work-around:

# imports
import xesmf as xe
import xcdat as xc
import numpy as np

# target grid
nlat = xc.create_axis('lat', np.arange(-88.5, 90, 2.5))
nlon = xc.create_axis('lon', np.arange(1.25, 360, 2.5))
ngrid = xc.create_grid(x=nlon, y=nlat)

# open / regrid
p = '/p/css03/esgf_publish/CMIP6/ScenarioMIP/NCAR/CESM2-WACCM/ssp585/r1i1p1f1/Omon/fgco2/gn/v20200702/'
ds = xc.open_mfdataset(p)

# regrid with xesmf directly
regridder = xe.Regridder(ds, ngrid, "conservative_normed", ignore_degenerate=True)
ds = regridder(ds)

# regenerate bounds
for b in ['bounds_lat', 'bounds_lon', 'lat_bnds', 'lon_bnds']:
        if b in ds.data_vars:
                ds = ds.drop_vars(b)
ds = ds.bounds.add_missing_bounds()

I think this might indicate that something changed with how xcdat passes information to xesmf (or xesmf changed).

@tomvothecoder
Copy link
Collaborator

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File ~/bin/anaconda3/envs/xcdat/lib/python3.12/site-packages/xesmf/frontend.py:75, in _get_lon_lat(ds)
     74 try:
---> 75     lon = ds.cf['longitude']
     76     lat = ds.cf['latitude']

I've isolated the code block in xESMF where the KeyError is being raised and found that it has not changed in 5 years.

https://github.com/pangeo-data/xESMF/blob/feebcfc11147eb00e3256924f0fded1485b7df60/xesmf/frontend.py#L68-L82

Also your workaround above might indicate that xCDAT is dropping or not copying over attributes to the lat/lon (which is a regression in behavior from previous versions I think?).

@tomvothecoder
Copy link
Collaborator

tomvothecoder commented Jan 7, 2025

Looking at the CF info via cf_xarray, both ngrid and ds have CF compliant information before calling the xCDAT regridder.

ngrid.cf

Coordinates:
             CF Axes: * X: ['lon']
                      * Y: ['lat']
                        Z, T: n/a

      CF Coordinates: * longitude: ['lon']
                      * latitude: ['lat']
                        vertical, time: n/a

       Cell Measures:   area, volume: n/a

      Standard Names:   n/a

              Bounds:   n/a

       Grid Mappings:   n/a

Data Variables:
       Cell Measures:   area, volume: n/a

      Standard Names:   n/a

              Bounds:   X: ['lon_bnds']
                        Y: ['lat_bnds']
                        lat: ['lat_bnds']
                        latitude: ['lat_bnds']
                        lon: ['lon_bnds']
                        longitude: ['lon_bnds']

       Grid Mappings:   n/a
ds.cf

Coordinates:
             CF Axes:   X: ['lon', 'nlon']
                        Y: ['lat', 'nlat']
                      * T: ['time']
                        Z: n/a

      CF Coordinates:   longitude: ['lon']
                        latitude: ['lat']
                      * time: ['time']
                        vertical: n/a

       Cell Measures:   area, volume: n/a

      Standard Names:   latitude: ['lat']
                        longitude: ['lon']
                      * time: ['time']

              Bounds:   n/a

       Grid Mappings:   n/a

Data Variables:
       Cell Measures:   area, volume: n/a

      Standard Names:   surface_downward_mass_flux_of_carbon_dioxide_expressed_as_carbon: ['fgco2']

              Bounds:   T: ['time_bnds']
                        X: ['lon_bnds', 'nlon_bnds']
                        Y: ['lat_bnds']
                        lat: ['lat_bnds']
                        latitude: ['lat_bnds']
                        lon: ['lon_bnds']
                        longitude: ['lon_bnds']
                        nlon: ['nlon_bnds']
                        time: ['time_bnds']

       Grid Mappings:   n/a

@pochedls
Copy link
Collaborator Author

pochedls commented Jan 7, 2025

@tomvothecoder – thank you for digging into this...before you get too far – can you reproduce the error? Maybe the issue is on my end (weird environment and old/new cf-xarray or something)?

@tomvothecoder
Copy link
Collaborator

@pochedls I can reproduce your issue with the latest main and xcdat 0.7.0.

I found the root cause of this issue. The dataset has two X and Y axes (nlat/lat, nlon/lon).

<xarray.Dataset> Size: 2GB
Dimensions:    (time: 3420, nlat: 384, nlon: 320, d2: 2, vertices: 4, bnds: 2)
Coordinates:
    lat        (nlat, nlon) float64 983kB dask.array<chunksize=(384, 320), meta=np.ndarray>
    lon        (nlat, nlon) float64 983kB dask.array<chunksize=(384, 320), meta=np.ndarray>
  * nlat       (nlat) int32 2kB 1 2 3 4 5 6 7 8 ... 378 379 380 381 382 383 384
  * nlon       (nlon) int32 1kB 1 2 3 4 5 6 7 8 ... 314 315 316 317 318 319 320
  * time       (time) object 27kB 2015-01-15 13:00:00.000007 ... 2299-12-15 1...

The input grid that is extracted (code here) from the dataset consists of nlat and nlon since they are the dimension coordinates (indicated by *). nlat and nlon do not have CF-compliant attributes, resulting in the error ValueError: dataset must include lon/lat or be CF-compliant raised by xESMF/CF-Xarray.

ds.nlat.attrs
{'long_name': 'cell index along second dimension', 'units': '1'}

ds.nlon.attrs
{'long_name': 'cell index along first dimension', 'units': '1', 'bounds': 'nlon_bnds'}

Meanwhile, lat and lon have the correct CF-compliant attributes, but they are auxiliary coordinates that are not considered.

ds.lat.attrs
{'axis': 'Y', 'bounds': 'lat_bnds', 'standard_name': 'latitude', 'title': 'Latitude', 'type': 'double', 'units': 'degrees_north', 'valid_max': np.float64(90.0), 'valid_min': np.float64(-90.0)}

ds.lon.attrs
{'axis': 'X', 'bounds': 'lon_bnds', 'standard_name': 'longitude', 'title': 'Longitude', 'type': 'double', 'units': 'degrees_east', 'valid_max': np.float64(360.0), 'valid_min': np.float64(0.0)}

Are lat and lon supposed to be the dimension coordinates rather than auxiliary coordinates?

@pochedls
Copy link
Collaborator Author

pochedls commented Jan 8, 2025

Got it. I think xcdat used to be able to sort this out since this data structure is what is used for unstructured grids (which can be regridded with ESMF). I'll see if I can find examples in which this did work in the past (I know we tested regridding sea ice or sea surface temperature).

@pochedls
Copy link
Collaborator Author

pochedls commented Jan 8, 2025

It is hard to trace, but I think that an example dataset that we used to be able to regrid, but is now problematic is this one:

/p/css03/esgf_publish/CMIP6/CMIP/NCAR/CESM2-WACCM/historical/r1i1p1f1/SImon/siconc/gn/v20190227/

I think @jasonb5 was able to get this working by resolving this issue.

@tomvothecoder tomvothecoder moved this from Todo to Next Up in xCDAT Development Jan 23, 2025
@pochedls
Copy link
Collaborator Author

pochedls commented Feb 1, 2025

One data point: the code above works on xcdat 0.5.0, xesmf 0.8.2, esmpy 8.4.2 (minor change to set ngrid = xc.create_grid(lat=np.arange(-88.75, 90, 2.5), lon=np.arange(1.25, 360, 2.5)) since the syntax was different then).

This version of xesmf and esmpy do not work with xcdat 0.6.1.

@tomvothecoder
Copy link
Collaborator

One data point: the code above works on xcdat 0.5.0, xesmf 0.8.2, esmpy 8.4.2 (minor change to set ngrid = xc.create_grid(lat=np.arange(-88.75, 90, 2.5), lon=np.arange(1.25, 360, 2.5)) since the syntax was different then).

This version of xesmf and esmpy do not work with xcdat 0.6.1.

@pochedls Thank you for checking this. I was trying to build an environment but ran into dependency conflicts. I'll analyze the differences in code between xcdat 0.5.0 and xcdat 0.6.1 that caused this GH issue.

@tomvothecoder
Copy link
Collaborator

tomvothecoder commented Feb 10, 2025

Root cause of issue

The dataset in this GitHub issue contains (nlat, lat) and (nlon, lon).

With xcdat>=0.6.1, when the input grid is created using ds.regridder.grid, lat and lon are dropped.

Coordinates:
    lat      (nlat, nlon) float64 dask.array<chunksize=(384, 320), meta=np.ndarray>
    lon      (nlat, nlon) float64 dask.array<chunksize=(384, 320), meta=np.ndarray>
  * nlat     (nlat) int32 1 2 3 4 5 6 7 8 9 ... 377 378 379 380 381 382 383 384
  * nlon     (nlon) int32 1 2 3 4 5 6 7 8 9 ... 313 314 315 316 317 318 319 320
  * time     (time) object 2015-01-15 13:00:00.000007 ... 2299-12-15 12:00:00

Unfortunately, lat and lon are the coordinates that have the CF-metadata (e.g., latitude) required for downstream operations by cf-xarray/xESMF. Since cf-xarray/xESMF cannot map to the required coordinates, xESMF raises ValueError: dataset must include lon/lat or be CF-compliant.

Code Traceback

  1. Get the input grid using _get_input_grid() to pass to xESMF
  2. _get_input_grid() line 326 drops extra dims on input grid using ds.regridder.grid
  3. The ds.regridder.grid property drops extra dimensions that are not index dimensions.
    • def grid(self) -> xr.Dataset:
      """
      Extract the `X`, `Y`, and `Z` axes from the Dataset and return a new
      ``xr.Dataset``.
      Returns
      -------
      xr.Dataset
      Containing grid axes.
      Raises
      ------
      ValueError
      If axis dimension coordinate variable is not correctly identified.
      ValueError
      If axis has multiple dimensions (only one is expected).
      Examples
      --------
      Import xCDAT:
      >>> import xcdat
      Open a dataset:
      >>> ds = xcdat.open_dataset("...")
      Extract grid from dataset:
      >>> grid = ds.regridder.grid
      """
      with xr.set_options(keep_attrs=True):
      coords = {}
      axis_names: List[CFAxisKey] = ["X", "Y", "Z"]
      for axis in axis_names:
      try:
      data, bnds = self._get_axis_data(axis)
      except KeyError:
      continue
      coords[data.name] = data.copy()
      if bnds is not None:
      coords[bnds.name] = bnds.copy()
      ds = xr.Dataset(coords, attrs=self._ds.attrs)
      ds = ds.bounds.add_missing_bounds(axes=["X", "Y", "Z"])
      return ds
  4. This input grid is passed to xESMF.
  5. xESMF raises ValueError: dataset must include lon/lat or be CF-compliant
     ---------------------------------------------------------------------------
     KeyError                                  Traceback (most recent call last)
     File ~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:75, in _get_lon_lat(ds)
          [74](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:74) try:
     ---> [75](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:75)     lon = ds.cf['longitude']
          [76](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:76)     lat = ds.cf['latitude']
     
     File ~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/cf_xarray/accessor.py:2349, in CFDatasetAccessor.__getitem__(self, key)
        [2318](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/cf_xarray/accessor.py:2318) """
        [2319](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/cf_xarray/accessor.py:2319) Index into a Dataset making use of CF attributes.
        [2320](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/cf_xarray/accessor.py:2320) 
        (...)
        [2347](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/cf_xarray/accessor.py:2347) Add additional keys by specifying "custom criteria". See :ref:`custom_criteria` for more.
        [2348](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/cf_xarray/accessor.py:2348) """
     -> [2349](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/cf_xarray/accessor.py:2349) return _getitem(self, key)
     
     File ~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/cf_xarray/accessor.py:941, in _getitem(accessor, key, skip)
         [940](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/cf_xarray/accessor.py:940) except KeyError:
     --> [941](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/cf_xarray/accessor.py:941)     raise KeyError(
         [942](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/cf_xarray/accessor.py:942)         f"{kind}.cf does not understand the key {k!r}. "
         [943](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/cf_xarray/accessor.py:943)         f"Use 'repr({kind}.cf)' (or '{kind}.cf' in a Jupyter environment) to see a list of key names that can be interpreted."
         [944](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/cf_xarray/accessor.py:944)     ) from None
     
     KeyError: "Dataset.cf does not understand the key 'longitude'. Use 'repr(Dataset.cf)' (or 'Dataset.cf' in a Jupyter environment) to see a list of key names that can be interpreted."
     
     During handling of the above exception, another exception occurred:
     
     ValueError                                Traceback (most recent call last)
     File /home/vo13/xCDAT/xcdat/qa/718-xesmf/qa.py:14
          [12](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/qa/718-xesmf/qa.py:12) p = "/p/css03/esgf_publish/CMIP6/ScenarioMIP/NCAR/CESM2-WACCM/ssp585/r1i1p1f1/Omon/fgco2/gn/v20200702/"
          [13](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/qa/718-xesmf/qa.py:13) ds = xc.open_mfdataset(p)
     ---> [14](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/qa/718-xesmf/qa.py:14) ds = ds.regridder.horizontal(
          [15](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/qa/718-xesmf/qa.py:15)     "fgco2", ngrid, tool="xesmf", method="conservative_normed", periodic=True
          [16](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/qa/718-xesmf/qa.py:16) )
     
     File ~/xCDAT/xcdat/xcdat/regridder/accessor.py:205, in RegridderAccessor.horizontal(self, data_var, output_grid, tool, **options)
         [203](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/accessor.py:203) input_grid = _get_input_grid(self._ds, data_var, ["X", "Y"])
         [204](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/accessor.py:204) regridder = regrid_tool(input_grid, output_grid, **options)
     --> [205](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/accessor.py:205) output_ds = regridder.horizontal(data_var, self._ds)
         [207](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/accessor.py:207) return output_ds
     
     File ~/xCDAT/xcdat/xcdat/regridder/xesmf.py:158, in XESMFRegridder.horizontal(self, data_var, ds)
         [153](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/xesmf.py:153) if input_da is None:
         [154](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/xesmf.py:154)     raise KeyError(
         [155](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/xesmf.py:155)         f"The data variable '{data_var}' does not exist in the dataset."
         [156](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/xesmf.py:156)     )
     --> [158](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/xesmf.py:158) regridder = xe.Regridder(
         [159](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/xesmf.py:159)     self._input_grid,
         [160](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/xesmf.py:160)     self._output_grid,
         [161](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/xesmf.py:161)     method=self._method,
         [162](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/xesmf.py:162)     **self._extra_options,
         [163](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/xesmf.py:163) )
         [165](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/xesmf.py:165) output_da = regridder(input_da, keep_attrs=True)
         [167](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/xCDAT/xcdat/xcdat/regridder/xesmf.py:167) output_ds = xr.Dataset({data_var: output_da}, attrs=ds.attrs)
     
     File ~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:919, in Regridder.__init__(self, ds_in, ds_out, method, locstream_in, locstream_out, periodic, parallel, **kwargs)
         [917](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:917)     grid_in, shape_in, input_dims = ds_to_ESMFlocstream(ds_in)
         [918](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:918) else:
     --> [919](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:919)     grid_in, shape_in, input_dims = ds_to_ESMFgrid(
         [920](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:920)         ds_in, need_bounds=need_bounds, periodic=periodic
         [921](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:921)     )
         [922](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:922) if locstream_out:
         [923](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:923)     grid_out, shape_out, output_dims = ds_to_ESMFlocstream(ds_out)
     
     File ~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:145, in ds_to_ESMFgrid(ds, need_bounds, periodic, append)
         [115](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:115) """
         [116](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:116) Convert xarray DataSet or dictionary to ESMF.Grid object.
         [117](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:117) 
        (...)
         [141](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:141) 
         [142](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:142) """
         [143](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:143) # use np.asarray(dr) instead of dr.values, so it also works for dictionary
     --> [145](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:145) lon, lat = _get_lon_lat(ds)
         [146](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:146) if hasattr(lon, 'dims'):
         [147](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:147)     if lon.ndim == 1:
     
     File ~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:80, in _get_lon_lat(ds)
          [76](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:76)     lat = ds.cf['latitude']
          [77](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:77) except (KeyError, AttributeError, ValueError):
          [78](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:78)     # KeyError if cfxr doesn't detect the coords
          [79](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:79)     # AttributeError if ds is a dict
     ---> [80](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:80)     raise ValueError('dataset must include lon/lat or be CF-compliant')
          [82](https://vscode-remote+ssh-002dremote-002bacme1.vscode-resource.vscode-cdn.net/home/vo13/xCDAT/xcdat/~/miniforge3/envs/xcdat_dev_718/lib/python3.12/site-packages/xesmf/frontend.py:82) return lon, lat
     
     ValueError: dataset must include lon/lat or be CF-compliant

Possible Solutions

  1. Make sure input grid has latitude and longitude attributes -- either manually adding or copying attributes over from auxiliary coordinates (if needed).

Additional Info

Diff between xcdat=0.5.0 and xcdat=0.6.1: v0.5.0...v0.6.1

@tomvothecoder tomvothecoder moved this from Next Up to In Progress in xCDAT Development Feb 10, 2025
@jasonb5
Copy link
Collaborator

jasonb5 commented Feb 18, 2025

@tomvothecoder @pochedls
I gave the xESMF raises ValueError: dataset must include lon/lat or be CF-compliant. issue a look. I'm going to guess we'll have this issue with any curvilinear grid.

I'm thinking there's a bug in get_dim_coords which in this case returns nlat and nlon when called with Y and X. I'm not sure if this is strictly a metadata issues. A straight call to cf_xarray doesn't work as it cannot distinguish between nlat and lat for the example of Y. But if it's called with ds.cf["lat"] the correct coordinate is returned, lat. As a test i replaced the get_dim_coords (

coord_var = get_dim_coords(self._ds, name)
and
coords = get_dim_coords(ds, dimension)
) with something along the lines of ds.cf["lat"] if name == "Y" else ds.cf["lon"]. This works and builds an input grid that xESMF can work with since lat and lon are in the input grid now.

The last issues is the bounds, the above change gets us a new issue.

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File ~/.local/lib/python3.10/site-packages/xesmf/frontend.py:95, in _get_lon_lat_bounds(ds)
     94 try:
---> 95     lon_bnds = ds.cf.get_bounds('longitude')
     96     lat_bnds = ds.cf.get_bounds('latitude')

File ~/.local/lib/python3.10/site-packages/cf_xarray/accessor.py:2463, in CFDatasetAccessor.get_bounds(self, key)
   2462 if not results:
-> 2463     raise KeyError(f"No results found for {key!r}.")
   2465 return self._obj[results[0] if len(results) == 1 else results]

KeyError: "No results found for 'longitude'."

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
Cell In[11], line 2
      1 import pdb
----> 2 pdb.run("ds_regrid = ds.regridder.horizontal('fgco2', ngrid, tool='xesmf', method='conservative_normed', periodic=True)")

File [~/conda-envs/xcdat-736/lib/python3.10/pdb.py:1607](https://nimbus.llnl.gov/user/jasonb5/lab/tree/conda-envs/xcdat-736/lib/python3.10/pdb.py#line=1606), in run(statement, globals, locals)
   1606 def run(statement, globals=None, locals=None):
-> 1607     Pdb().run(statement, globals, locals)

File [~/conda-envs/xcdat-736/lib/python3.10/bdb.py:598](https://nimbus.llnl.gov/user/jasonb5/lab/tree/conda-envs/xcdat-736/lib/python3.10/bdb.py#line=597), in Bdb.run(self, cmd, globals, locals)
    596 sys.settrace(self.trace_dispatch)
    597 try:
--> 598     exec(cmd, globals, locals)
    599 except BdbQuit:
    600     pass

File <string>:1

File [~/devel/xcdat/xcdat/regridder/accessor.py:240](https://nimbus.llnl.gov/user/jasonb5/lab/tree/devel/xcdat/xcdat/regridder/accessor.py#line=239), in RegridderAccessor.horizontal(self, data_var, output_grid, tool, **options)
    238 input_grid = _get_input_grid(self._ds, data_var, ["X", "Y"])
    239 regridder = regrid_tool(input_grid, output_grid, **options)
--> 240 output_ds = regridder.horizontal(data_var, self._ds)
    242 return output_ds

File [~/devel/xcdat/xcdat/regridder/xesmf.py:158](https://nimbus.llnl.gov/user/jasonb5/lab/tree/devel/xcdat/xcdat/regridder/xesmf.py#line=157), in XESMFRegridder.horizontal(self, data_var, ds)
    153 if input_da is None:
    154     raise KeyError(
    155         f"The data variable '{data_var}' does not exist in the dataset."
    156     )
--> 158 regridder = xe.Regridder(
    159     self._input_grid,
    160     self._output_grid,
    161     method=self._method,
    162     **self._extra_options,
    163 )
    165 output_da = regridder(input_da, keep_attrs=True)
    167 output_ds = xr.Dataset({data_var: output_da}, attrs=ds.attrs)

File ~/.local/lib/python3.10/site-packages/xesmf/frontend.py:919, in Regridder.__init__(self, ds_in, ds_out, method, locstream_in, locstream_out, periodic, parallel, **kwargs)
    917     grid_in, shape_in, input_dims = ds_to_ESMFlocstream(ds_in)
    918 else:
--> 919     grid_in, shape_in, input_dims = ds_to_ESMFgrid(
    920         ds_in, need_bounds=need_bounds, periodic=periodic
    921     )
    922 if locstream_out:
    923     grid_out, shape_out, output_dims = ds_to_ESMFlocstream(ds_out)

File ~/.local/lib/python3.10/site-packages/xesmf/frontend.py:167, in ds_to_ESMFgrid(ds, need_bounds, periodic, append)
    164     grid = Grid.from_xarray(lon.T, lat.T, periodic=periodic, mask=None)
    166 if need_bounds:
--> 167     lon_b, lat_b = _get_lon_lat_bounds(ds)
    168     lon_b, lat_b = as_2d_mesh(np.asarray(lon_b), np.asarray(lat_b))
    169     add_corner(grid, lon_b.T, lat_b.T)

File ~/.local/lib/python3.10/site-packages/xesmf/frontend.py:100, in _get_lon_lat_bounds(ds)
     97 except KeyError:  # bounds are not already present
     98     if ds.cf['longitude'].ndim > 1:
     99         # We cannot infer 2D bounds, raise KeyError as custom "lon_b" is missing.
--> 100         raise KeyError('lon_b')
    101     lon_name = ds.cf['longitude'].name
    102     lat_name = ds.cf['latitude'].name

KeyError: 'lon_b'

This is probably just an issue in get_bounds since it's adding only nlon_bnds and not lat_bnds and lon_bnds which are the correct ones.

@jasonb5
Copy link
Collaborator

jasonb5 commented Feb 18, 2025

Looks like lat and lon are dropped here

dim_coord_keys = list(set(index_keys) & set(coord_keys))
since they are not present in obj.indexes.keys().

I'm wondering if the solution is to update the regridding code to distinguish between rectilinear and curvilinear grids and change the behavior there.

@pochedls
Copy link
Collaborator Author

I have not stepped through the code (so this may be off base), but in looking at your comments (and maybe from past versions of code), it might be useful to have/use a .cf method that operates on the variable dataarray (where it uses the data array dims information to constrain what axes are returned). This worked in 0.5.0 somehow (and it works if we call xESMF directly), so I'm hoping we can do something simple to ensure we consistently pass the correct information to xesmf.

@tomvothecoder
Copy link
Collaborator

tomvothecoder commented Feb 19, 2025

I'm thinking there's a bug in get_dim_coords which in this case returns nlat and nlon when called with Y and X. I'm not sure if this is strictly a metadata issues.

I think get_dim_coords() is working as intended because it is designed to get the dimension/index coordinates for an axes.
For some reason, nlat and nlon are being picked up by cf-xarray due to CF metadata attributes (I think long_name attr) AND they're dim coords which means xCDAT returns them.

The issue here sounds we need to add support for curvilinear grids during regridding, which can have (nlat, lat) and (nlon, lon) style dimensions. In the context of get_dim_coords(), returning nlat and nlon is technically correct from a data structure perspective. However, the user actually wants lat and lon because those provide the actual values and CF metadata.

it might be useful to have/use a .cf method that operates on the variable dataarray (where it uses the data array dims information to constrain what axes are returned).

If we take a look at the variable's dims, it is (time, nlat, nlon). This means nlat/nlon would be returned for the axes.

Image

As mentioned earlier, cf-xarray/xCDAT recognizes nlat and nlon with CF metadata. We would probably need to specify another cf-xarray method such as ds.cf["latitude"] to only get lat (shown below).

Image

  • v0.5.0 code - I think there was a bug here before where multiple coordinates were being returned for an axis, that's why we changed the code in v0.6.0+ to use _get_input_grid() which calls get_dim_coords() (Fixes handling multiple z axes when vertically regridding #525)
  • v0.8.0 code
    • input_grid = _get_input_grid(self._ds, data_var, ["X", "Y"])
      regridder = regrid_tool(input_grid, output_grid, **options)
      output_ds = regridder.horizontal(data_var, self._ds)
    • def _get_input_grid(ds: xr.Dataset, data_var: str, dup_check_dims: List[CFAxisKey]):
      """
      Extract the grid from ``ds``.
      This function will remove any duplicate dimensions leaving only dimensions
      used by the ``data_var``. All extraneous dimensions and variables are
      dropped, returning only the grid.
      Parameters
      ----------
      ds : xr.Dataset
      Dataset to extract grid from.
      data_var : str
      Name of target data variable.
      dup_check_dims : List[CFAxisKey]
      List of dimensions to check for duplicates.
      Returns
      -------
      xr.Dataset
      Dataset containing grid dataset.
      """
      to_drop = []
      all_coords = set(ds.coords.keys())
      for dimension in dup_check_dims:
      coords = get_dim_coords(ds, dimension)
      if isinstance(coords, xr.Dataset):
      coord = set([get_dim_coords(ds[data_var], dimension).name])
      dimension_coords = set(ds.cf[[dimension]].coords.keys())
      # need to take the intersection after as `ds.cf[["Z"]]` will hand back data variables
      to_drop += list(dimension_coords.difference(coord).intersection(all_coords))
      input_grid = ds.drop_dims(to_drop)
      # drops extra dimensions on input grid
      grid = input_grid.regridder.grid
      # preserve mask on grid
      if "mask" in ds:
      grid["mask"] = ds["mask"].copy()
      return grid

Looks like lat and lon are dropped here

xcdat/xcdat/axis.py

Line 135 in b260fcb
dim_coord_keys = list(set(index_keys) & set(coord_keys))
since they are not present in obj.indexes.keys().

I'm wondering if the solution is to update the regridding code to distinguish between rectilinear and curvilinear grids and change the behavior there.

We'll need to update _get_input_grid() to support curvilinear grids.

This sounds like a potential path forward.

Option 1

  • If the grid type is rectilinear, use get_dim_coords() as done right now
  • If the grid is curvilinear (with nlat/nlon), use ds.cf["latitude"]/ds.cf["longitude"] -- This solution will need to be verified with other datasets on a curvilinear grid though.

Option 2

Or we can just use cf-xarray directly for both cases, which would simplify the code

@tomvothecoder
Copy link
Collaborator

Continuing my comment above, I'm currently exploring possible solutions. Another one is updating get_dim_coords() to properly handle the curvilinear grid. This might mean detecting if the index coordinate is nlat/nlon, then substituting it with the correct lat/lon coordinates. If we do this, then we should update the function docstring and/or raise a logger warning because technically the dim coords are not being returned.

@jasonb5
Copy link
Collaborator

jasonb5 commented Feb 24, 2025

@tomvothecoder Sounds like get_dim_coords() is working as intended. I'm fine handling this in the regrid code. Looks like there's some documentation in CF Conventions related to 4-sided and p-sided grids. I can probably use this to build some heuristics to differentiate between rectilinear and curvilinear grids.

I'll add some properties to the regridder accessor e.g. is_rectilinear_grid, is_curvilinear_grid to test.

@tomvothecoder
Copy link
Collaborator

@tomvothecoder Sounds like get_dim_coords() is working as intended. I'm fine handling this in the regrid code. Looks like there's some documentation in CF Conventions related to 4-sided and p-sided grids. I can probably use this to build some heuristics to differentiate between rectilinear and curvilinear grids.

I'll add some properties to the regridder accessor e.g. is_rectilinear_grid, is_curvilinear_grid to test.

Thanks @jasonb5. Let me know if you need anything.

@dcherian
Copy link

think get_dim_coords() is working as intended because it is designed to get the dimension/index coordinates for an axes.
For some reason, nlat and nlon are being picked up by cf-xarray due to CF metadata attributes (I think long_name attr) AND they're dim coords which means xCDAT returns them.

Another option is to use SGRID which would allow you to distinguish between the two: https://cf-xarray.readthedocs.io/en/latest/sgrid_ugrid.html . Cf-xarray does not interpret long_name.

@tomvothecoder
Copy link
Collaborator

tomvothecoder commented Mar 17, 2025

think get_dim_coords() is working as intended because it is designed to get the dimension/index coordinates for an axes.
For some reason, nlat and nlon are being picked up by cf-xarray due to CF metadata attributes (I think long_name attr) AND they're dim coords which means xCDAT returns them.

Another option is to use SGRID which would allow you to distinguish between the two: cf-xarray.readthedocs.io/en/latest/sgrid_ugrid.html . Cf-xarray does not interpret long_name.

Nice! Thanks for the rec @dcherian. We'll look into SGRID.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug Inconsistencies or issues which will cause an issue or problem for users or implementors.
Projects
Status: In Progress
Development

Successfully merging a pull request may close this issue.

4 participants