Skip to content

Variable endpoints undefined when order is not ascending or descending #594

Closed

Description

@malmans2

Today I ran into this error:

UnboundLocalError: cannot access local variable 'endpoints' where it is not associated with a value

Sorry I don't have a minimal reproducible example yet. I'll try to make one ASAP.
Just posting in case others already know what's going on.

From what I can tell, this happens because the case where order not in ["ascending", "descending"] isn't covered.
See:

if order == "ascending":
endpoints = np.minimum(bounds[..., :, 0], bounds[..., :, 1])
last_endpoint = np.maximum(bounds[..., -1, 0], bounds[..., -1, 1])
elif order == "descending":
endpoints = np.maximum(bounds[..., :, 0], bounds[..., :, 1])
last_endpoint = np.minimum(bounds[..., -1, 0], bounds[..., -1, 1])

This seems to have been introduced with PR #579
Maybe @tomvothecoder has thoughts?

Activity

  1. tomvothecoder commented on Sep 15, 2025

    @tomvothecoder
    Contributor

    Hi @malmans2, thanks for reporting this issue. I'll try debugging once you provide a minimal reproducible example.

    I think the order is "mixed", so there might be some issues with the bounds in your dataset. This code block determines the order of bounds:

    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

    However, the code you pointed to should still handle that case (e.g., maybe raise an error).

  2. malmans2 commented on Sep 15, 2025

    @malmans2
    MemberAuthor

    It looks like the issue occurs when longitude is in the 0-360 format, centered around 0.

    Here’s the MRE:

    import cf_xarray as cfxr
    import xarray as xr
    
    ds = xr.Dataset(
        {
            "latitude": ("latitude", [-90, 0, 90]),
            "longitude": ("longitude", [359, 0, 1]),
        }
    )
    ds = ds.cf.add_bounds(["longitude", "latitude"])
    lon_bnds = ds.cf.get_bounds("longitude")
    lon_b = cfxr.bounds_to_vertices(
        lon_bnds,
        ds.cf.get_bounds_dim_name("longitude"),
        order=None,
    )

    The MRE is basically a simplified version of this internal function of xesmf, which is how I discovered the problem: https://github.com/pangeo-data/xESMF/blob/f2eeeb2071f4f1386dd0852ddb2696568134e387/xesmf/frontend.py#L85-L105

    Traceback
    ---------------------------------------------------------------------------
    UnboundLocalError                         Traceback (most recent call last)
    Cell In[1], line 12
         10 ds = ds.cf.add_bounds(["longitude", "latitude"])
         11 lon_bnds = ds.cf.get_bounds("longitude")
    ---> 12 lon_b = cfxr.bounds_to_vertices(
         13     lon_bnds,
         14     ds.cf.get_bounds_dim_name("longitude"),
         15     order=None,
         16 )
    
    File [/data/common/miniforge3/envs/wp5/lib/python3.12/site-packages/cf_xarray/helpers.py:183](http://localhost:5678/data/common/miniforge3/envs/wp5/lib/python3.12/site-packages/cf_xarray/helpers.py#line=182), in bounds_to_vertices(bounds, bounds_dim, core_dims, order)
        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,
        186     input_core_dims=[core_dims + [bounds_dim]],
        187     dask="parallelized",
        188     kwargs={
        189         "n_core_dims": n_core_dims,
        190         "nbounds": nbounds,
        191         "order": order,
        192         "core_dim_orders": core_dim_orders,
        193     },
        194     output_core_dims=[output_core_dims],
        195     dask_gufunc_kwargs=dict(output_sizes=output_sizes),
        196     output_dtypes=[bounds.dtype],
        197 )
    
    File [/data/common/miniforge3/envs/wp5/lib/python3.12/site-packages/xarray/computation/apply_ufunc.py:1267](http://localhost:5678/data/common/miniforge3/envs/wp5/lib/python3.12/site-packages/xarray/computation/apply_ufunc.py#line=1266), in apply_ufunc(func, input_core_dims, output_core_dims, exclude_dims, vectorize, join, dataset_join, dataset_fill_value, keep_attrs, kwargs, dask, output_dtypes, output_sizes, meta, dask_gufunc_kwargs, on_missing_core_dim, *args)
       1265 # feed DataArray apply_variable_ufunc through apply_dataarray_vfunc
       1266 elif any(isinstance(a, DataArray) for a in args):
    -> 1267     return apply_dataarray_vfunc(
       1268         variables_vfunc,
       1269         *args,
       1270         signature=signature,
       1271         join=join,
       1272         exclude_dims=exclude_dims,
       1273         keep_attrs=keep_attrs,
       1274     )
       1275 # feed Variables directly through apply_variable_ufunc
       1276 elif any(isinstance(a, Variable) for a in args):
    
    File [/data/common/miniforge3/envs/wp5/lib/python3.12/site-packages/xarray/computation/apply_ufunc.py:310](http://localhost:5678/data/common/miniforge3/envs/wp5/lib/python3.12/site-packages/xarray/computation/apply_ufunc.py#line=309), in apply_dataarray_vfunc(func, signature, join, exclude_dims, keep_attrs, *args)
        305 result_coords, result_indexes = build_output_coords_and_indexes(
        306     args, signature, exclude_dims, combine_attrs=keep_attrs
        307 )
        309 data_vars = [getattr(a, "variable", a) for a in args]
    --> 310 result_var = func(*data_vars)
        312 out: tuple[DataArray, ...] | DataArray
        313 if signature.num_outputs > 1:
    
    File [/data/common/miniforge3/envs/wp5/lib/python3.12/site-packages/xarray/computation/apply_ufunc.py:818](http://localhost:5678/data/common/miniforge3/envs/wp5/lib/python3.12/site-packages/xarray/computation/apply_ufunc.py#line=817), in apply_variable_ufunc(func, signature, exclude_dims, dask, output_dtypes, vectorize, keep_attrs, dask_gufunc_kwargs, *args)
        813 elif vectorize:
        814     func = _vectorize(
        815         func, signature, output_dtypes=output_dtypes, exclude_dims=exclude_dims
        816     )
    --> 818 result_data = func(*input_data)
        820 if signature.num_outputs == 1:
        821     result_data = (result_data,)
    
    File [/data/common/miniforge3/envs/wp5/lib/python3.12/site-packages/cf_xarray/helpers.py:274](http://localhost:5678/data/common/miniforge3/envs/wp5/lib/python3.12/site-packages/cf_xarray/helpers.py#line=273), in _bounds_helper(values, n_core_dims, nbounds, order, core_dim_orders)
        271         vertex_vals = np.block([[bot_left, bot_right], [top_left, top_right]])
        272 elif n_core_dims == 1 and nbounds == 2:
        273     # Middle points case (1D lat[/lon](http://localhost:5678/lon))
    --> 274     vertex_vals = _get_ordered_vertices(values, core_dim_orders)
        276 return vertex_vals
    
    File [/data/common/miniforge3/envs/wp5/lib/python3.12/site-packages/cf_xarray/helpers.py:333](http://localhost:5678/data/common/miniforge3/envs/wp5/lib/python3.12/site-packages/cf_xarray/helpers.py#line=332), in _get_ordered_vertices(bounds, core_dim_orders)
        329         endpoints = np.maximum(bounds[..., :, 0], bounds[..., :, 1])
        330         last_endpoint = np.minimum(bounds[..., -1, 0], bounds[..., -1, 1])
        332     vertices = np.concatenate(
    --> 333         [endpoints, np.expand_dims(last_endpoint, axis=-1)], axis=-1
        334     )
        336 vertices = _sort_vertices(vertices, order)
        338 return vertices
    
    UnboundLocalError: cannot access local variable 'endpoints' where it is not associated with a value
  3. tomvothecoder commented on Sep 18, 2025

    @tomvothecoder
    Contributor

    It looks like this issue shows up when longitude is in the 0–360 format, centered around 0.

    @malmans2 @aulemahal This is an edge case we didn’t cover in PR #579. The coordinates are circular (0–360), and the 1-D core dimension isn’t strictly monotonic because it crosses the seam at 0°. In cf_xarray.helpers._get_core_dim_orders, that situation is inferred as "mixed" order.

    cf_xarray.helpers._get_ordered_vertices doesn’t handle "mixed" order (e.g., [359, 0, 1]), so endpoints is never set and we end up with an UnboundLocalError.

    I’m planning to open a PR to raise a clear ValueError in bounds_to_vertices whenever a 1-D core dimension isn’t strictly increasing or decreasing. Longer term, the cf-xarray devs might consider opt-in support for circular axes, e.g. bounds_to_vertices(..., circular_period=360.0, start=None). With circular_period set, cf-xarray could detect circular monotonicity, rotate away from the seam, and proceed safely.

    Suggestions

    • Your script: Normalize longitudes to a monotonic convention before regridding with xESMF (short-term fix).

      • Simplest: convert to [-180, 180) and sortby("longitude").
      • If you need 0–360, roll so the seam isn’t inside the array, then sort.
    • xesmf: Pre-normalize/unwrap coords (lon, lat if needed) before calling bounds_to_vertices.

      • Detect circular longitude grids, then either:
        • convert to [-180, 180) and sort, or
        • roll within 0–360 to move the seam away, then sort.
      • If cf-xarray adds circular_period, pass it through; otherwise handle normalization inside xESMF.
      • Add validation + helpful errors for non-monotonic or seam-crossing inputs.
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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions