Skip to content

v0.10.7 breaks bounds_to_vertices for cftime objects #581

Closed

Description

@larsbuntemeyer

I just realized my CI broke because, e.g., this does not work since the latest release (worked before):

# Example: xarray Dataset with CF-compliant time bounds using cftime
import numpy as np
import xarray as xr
import cf_xarray as cfxr
import cftime

# Create cftime objects for monthly bounds
periods = 3
start = cftime.DatetimeGregorian(2000, 1, 1)
edges = [cftime.DatetimeGregorian(2000, m, 1) for m in range(1, periods + 2)]

# Bounds as [start, end) for each month
bnds = np.array([[edges[i], edges[i+1]] for i in range(periods)])
mid = np.array([edges[i] + (edges[i+1] - edges[i]) / 2 for i in range(periods)])

# Sample data
values = xr.DataArray(np.arange(periods, dtype=float), dims=("time",), coords={"time": mid})

# Build dataset with CF-style bounds
ds = xr.Dataset(
    {"foo": values},
    coords={
        "time": ("time", mid, {"bounds": "time_bnds"}),
        "time_bnds": (("time", "bnds"), bnds),
        "bnds": ("bnds", [0, 1])
    }
)

# Create vertices from bounds
cfxr.bounds_to_vertices(ds.time_bnds, "bnds")

gives...

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[3], [line 30](vscode-notebook-cell:?execution_count=3&line=30)
     20 ds = xr.Dataset(
     21     {"foo": values},
     22     coords={
   (...)     26     }
     27 )
     29 # Create vertices from bounds
---> 30 cfxr.bounds_to_vertices(ds.time_bnds, "bnds")

File ~/python/packages/cf-xarray/cf_xarray/helpers.py:181, in bounds_to_vertices(bounds, bounds_dim, core_dims, order)
    174     raise ValueError(
    175         f"Bounds format not understood. Got {bounds.dims} with shape {bounds.shape}."
    176     )
    178 core_dim_coords = {
    179     dim: bounds.coords[dim].values for dim in core_dims if dim in bounds.coords
    180 }
--> 181 core_dim_orders = _get_core_dim_orders(core_dim_coords)
    183 return xr.apply_ufunc(
    184     _bounds_helper,
    185     bounds,
   (...)    196     output_dtypes=[bounds.dtype],
    197 )

File ~/python/packages/cf-xarray/cf_xarray/helpers.py:240, in _get_core_dim_orders(core_dim_coords)
    237 if nonzero_diffs.size == 0:
    238     # All values are equal, treat as ascending
    239     core_dim_orders[dim] = "ascending"
--> 240 elif np.all(nonzero_diffs > 0):
    241     core_dim_orders[dim] = "ascending"
    242 elif np.all(nonzero_diffs < 0):

TypeError: '>' not supported between instances of 'datetime.timedelta' and 'int'

It still, however, works with usual datetime objects.

Activity

  1. larsbuntemeyer commented on Aug 15, 2025

    @larsbuntemeyer
    ContributorAuthor

    It seems with #579, we need to determine the order of an array of cftime objects or datetime timedelta respectivley in here

    def _get_core_dim_orders(core_dim_coords: dict[str, np.ndarray]) -> dict[str, str]:
    """
    Determine the order (ascending, descending, or mixed) of each core dimension
    based on its coordinates.
    Repeated (equal) coordinates are ignored when determining the order. If all
    coordinates are equal, the order is treated as "ascending".
    Parameters
    ----------
    core_dim_coords : dict of str to np.ndarray
    A dictionary mapping dimension names to their coordinate arrays.
    Returns
    -------
    core_dim_orders : dict of str to str
    A dictionary mapping each dimension name to a string indicating the order:
    - "ascending": strictly increasing (ignoring repeated values)
    - "descending": strictly decreasing (ignoring repeated values)
    - "mixed": neither strictly increasing nor decreasing (ignoring repeated values)
    """
    core_dim_orders = {}
    for dim, coords in core_dim_coords.items():
    diffs = np.diff(coords)
    # Handle datetime64 and timedelta64 safely for both numpy 1.26.4 and numpy 2
    if np.issubdtype(coords.dtype, np.datetime64) or np.issubdtype(
    coords.dtype, np.timedelta64
    ):
    # Cast to float64 for safe comparison
    diffs_float = diffs.astype("float64")
    nonzero_diffs = diffs_float[diffs_float != 0]
    else:
    zero = 0
    nonzero_diffs = diffs[diffs != zero]
    if nonzero_diffs.size == 0:
    # All values are equal, treat as ascending
    core_dim_orders[dim] = "ascending"
    elif np.all(nonzero_diffs > 0):
    core_dim_orders[dim] = "ascending"
    elif np.all(nonzero_diffs < 0):
    core_dim_orders[dim] = "descending"
    else:
    core_dim_orders[dim] = "mixed"
    return core_dim_orders

    timedelta does not cast to float which seems to be the problem here...

  2. larsbuntemeyer commented on Aug 15, 2025

    @larsbuntemeyer
    ContributorAuthor

    Seems like we could use total_seconds to cast cftime diffs to float.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions