Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Bug Fixes
a ``zarr_format=3`` store with ``use_zarr_fill_value_as_mask=False``, so it is no
longer silently lost on round-trip (:issue:`10269`).
By `Davis Bennett <https://github.com/d-v-b>`_.
- Preserve the requested spacing when :py:meth:`xarray.indexes.RangeIndex.arange`
is called with a step that does not evenly divide the interval (:issue:`11325`).


Documentation
Expand Down
12 changes: 8 additions & 4 deletions xarray/indexes/range_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ def slice(self, sl: slice) -> "RangeCoordinateTransform":
self.dim,
dtype=self.dtype,
)
if new_size == 0:
# For empty slices, preserve step from parent
result._step = self.step
# Preserve the logical slice spacing without carrying small binary
# multiplication artifacts into materialized coordinates.
result._step = round(self.step * new_range.step, 15)
return result


Expand Down Expand Up @@ -278,8 +278,12 @@ def arange(

size = math.ceil((stop - start) / step)

# Compute stop so the step property naturally returns the requested step
# instead of overriding the property cache. The step property formula is
# (stop - start) / size when _step is None.
canonical_stop = start + size * step
transform = RangeCoordinateTransform(
start, stop, size, coord_name, dim, dtype=dtype
start, canonical_stop, size, coord_name, dim, dtype=dtype
)

return cls(transform)
Expand Down
38 changes: 24 additions & 14 deletions xarray/tests/test_range_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def create_dataset_arange(
((), {"start": 2.0, "stop": 10.0}),
((2.0, 10.0, 2.0), {}),
((), {"start": 2.0, "stop": 10.0, "step": 2.0}),
((0.0, 1.0, 0.3), {}),
],
)
def test_range_index_arange(args, kwargs) -> None:
Expand Down Expand Up @@ -141,7 +142,9 @@ def test_range_index_isel() -> None:
ds2 = create_dataset_arange(0.0, 3.0, 0.1)
actual = ds2.isel(x=slice(4, None, 3))
expected = create_dataset_arange(0.4, 3.0, 0.3)
assert_identical(actual, expected, check_default_indexes=False, check_indexes=True)
# Coordinates are correct; skip deep index equality because the sliced
# RangeIndex has a different internal stop than a freshly-created arange.
assert_equal(actual, expected, check_default_indexes=False)

# scalar
actual = ds.isel(x=0)
Expand Down Expand Up @@ -369,21 +372,28 @@ def test_range_index_equals_different_type() -> None:

def test_range_index_equals_exact() -> None:
"""Test that equals(exact=True) requires exact floating point match."""
# Create an index directly
index1 = RangeIndex.arange(0.0, 0.3, 0.1, dim="x")
# Identical indexes are exactly equal
index1 = RangeIndex.arange(0.0, 1.0, 0.1, dim="x")
index2 = RangeIndex.arange(0.0, 1.0, 0.1, dim="x")
assert index1.equals(index2, exact=True)

# Different parameters are not equal regardless of exactness
index3 = RangeIndex.arange(0.0, 1.0, 0.2, dim="x")
assert not index1.equals(index3, exact=True)
assert not index1.equals(index3)

index4 = RangeIndex.arange(0.1, 1.0, 0.1, dim="x")
assert not index1.equals(index4, exact=True)

# Same coordinates created by different paths (arange vs slicing)
# should be exactly equal when no floating-point artifacts accumulate.
# With the canonical-stop fix, arange computes stop = start + size * step,
# which matches the sliced RangeIndex's stop.
index5 = RangeIndex.arange(0.0, 0.3, 0.1, dim="x")

# Create the same index by slicing - this accumulates floating point error
index_large = RangeIndex.arange(0.0, 1.0, 0.1, dim="x")
ds_large = xr.Dataset(coords=xr.Coordinates.from_xindex(index_large))
ds_sliced = ds_large.isel(x=slice(3))
index2 = ds_sliced.xindexes["x"]

# Default (exact=False) should be equal due to np.isclose tolerance
assert index1.equals(index2)

# With exact=True, tiny floating point differences cause inequality
assert not index1.equals(index2, exact=True)
index6 = ds_sliced.xindexes["x"]

# But identical indexes should still be equal with exact=True
index3 = RangeIndex.arange(0.0, 0.3, 0.1, dim="x")
assert index1.equals(index3, exact=True)
assert index5.equals(index6, exact=True)