From a4d70d08276498a0526eb63315a104a00cc64abd Mon Sep 17 00:00:00 2001 From: Alister Trabattoni Date: Thu, 28 May 2026 15:06:33 +0200 Subject: [PATCH 1/2] Fix slow `is_monotonic` for `Interp` and `Sampled` coordinate object. --- tests/coordinates/test_dense.py | 11 +++++++++++ tests/coordinates/test_interp.py | 22 ++++++++++++++++++++++ tests/coordinates/test_sampled.py | 23 +++++++++++++++++++++++ xdas/coordinates/interp.py | 4 ++++ xdas/coordinates/sampled.py | 4 ++++ 5 files changed, 64 insertions(+) diff --git a/tests/coordinates/test_dense.py b/tests/coordinates/test_dense.py index 6701d88..d18ab62 100644 --- a/tests/coordinates/test_dense.py +++ b/tests/coordinates/test_dense.py @@ -157,3 +157,14 @@ def test_from_block(self): coord = DenseCoordinate.from_block(0, 5, 1, dim="x") expected = DenseCoordinate([0, 1, 2, 3, 4], dim="x") assert coord.equals(expected) + + def test_is_monotonic_increasing(self): + assert DenseCoordinate([1, 2, 3]).is_monotonic_increasing() + assert not DenseCoordinate([1, 3, 2]).is_monotonic_increasing() + t0 = np.datetime64("2000-01-01T00:00:00") + times = np.array([t0, t0 + np.timedelta64(1, "s"), t0 + np.timedelta64(2, "s")]) + assert DenseCoordinate(times).is_monotonic_increasing() + times_bad = np.array( + [t0, t0 + np.timedelta64(2, "s"), t0 + np.timedelta64(1, "s")] + ) + assert not DenseCoordinate(times_bad).is_monotonic_increasing() diff --git a/tests/coordinates/test_interp.py b/tests/coordinates/test_interp.py index 4390dbc..ce69107 100644 --- a/tests/coordinates/test_interp.py +++ b/tests/coordinates/test_interp.py @@ -444,3 +444,25 @@ def test_get_split_indices_overlaps_with_tolerance(self): result = coord.get_split_indices(kind="overlaps", tolerance=0.5) assert isinstance(result, np.ndarray) np.testing.assert_array_equal(result, [5], strict=True) + + def test_is_monotonic_increasing_true(self): + coord = InterpCoordinate( + {"tie_indices": [0, 4, 5, 9], "tie_values": [0.0, 4.0, 5.0, 9.0]} + ) + assert coord.is_monotonic_increasing() is True + + def test_is_monotonic_increasing_false(self): + coord = InterpCoordinate( + {"tie_indices": [0, 4, 5, 9], "tie_values": [0.0, 4.0, 3.0, 7.0]} + ) + assert coord.is_monotonic_increasing() is False + + def test_is_monotonic_increasing_multi_segment(self): + # Three segments all strictly increasing — must not raise ValueError from bool() + coord = InterpCoordinate( + { + "tie_indices": [0, 4, 5, 9, 10, 14], + "tie_values": [0.0, 4.0, 5.0, 9.0, 10.0, 14.0], + } + ) + assert coord.is_monotonic_increasing() is True diff --git a/tests/coordinates/test_sampled.py b/tests/coordinates/test_sampled.py index 779b57f..e22252a 100644 --- a/tests/coordinates/test_sampled.py +++ b/tests/coordinates/test_sampled.py @@ -984,3 +984,26 @@ def test_get_split_indices_overlaps_with_tolerance(self): result = coord.get_split_indices(kind="overlaps", tolerance=1.0) assert isinstance(result, np.ndarray) np.testing.assert_array_equal(result, [5], strict=True) + + def test_is_monotonic_increasing_true(self): + coord = SampledCoordinate( + {"tie_values": [0.0, 5.0], "tie_lengths": [5, 5], "sampling_interval": 1.0} + ) + assert coord.is_monotonic_increasing() is True + + def test_is_monotonic_increasing_false(self): + coord = SampledCoordinate( + {"tie_values": [0.0, 2.0], "tie_lengths": [5, 5], "sampling_interval": 1.0} + ) + assert coord.is_monotonic_increasing() is False + + def test_is_monotonic_increasing_multi_segment(self): + # Three segments all increasing — must not raise ValueError from bool() + coord = SampledCoordinate( + { + "tie_values": [0.0, 5.0, 11.0], + "tie_lengths": [5, 5, 5], + "sampling_interval": 1.0, + } + ) + assert coord.is_monotonic_increasing() is True diff --git a/xdas/coordinates/interp.py b/xdas/coordinates/interp.py index debf036..1dc12da 100644 --- a/xdas/coordinates/interp.py +++ b/xdas/coordinates/interp.py @@ -216,6 +216,10 @@ def get_sampling_interval(self, cast=True): delta = delta / np.timedelta64(1, "s") return delta + def is_monotonic_increasing(self): + """Return ``True`` if no segment boundary exhibits a backward jump.""" + return not self.get_split_indices("overlaps", tolerance=False).size + def equals(self, other): """Return ``True`` if *other* has identical tie points, dim, and dtype.""" return ( diff --git a/xdas/coordinates/sampled.py b/xdas/coordinates/sampled.py index 2b6e7ac..06a9c6f 100644 --- a/xdas/coordinates/sampled.py +++ b/xdas/coordinates/sampled.py @@ -263,6 +263,10 @@ def get_sampling_interval(self, cast=True): delta = delta / np.timedelta64(1, "s") return delta + def is_monotonic_increasing(self): + """Return ``True`` if no segment starts below the end of the previous one.""" + return not self.get_split_indices("overlaps", tolerance=False).size + def equals(self, other): """Return ``True`` if *other* has identical tie values, lengths, sampling interval, dim, and dtype.""" return ( From 41c8b6eaee7486762e9b6f36334d3e529278e555 Mon Sep 17 00:00:00 2001 From: Alister Trabattoni Date: Thu, 28 May 2026 15:11:16 +0200 Subject: [PATCH 2/2] Update release-notes.py --- docs/release-notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index d7f8fd4..c3449ba 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ ## 0.2.7 +### Bug Fixes +- Fix a regression introduced in 0.2.6 where `is_monotonic` was significantly degrading `.sel` performance. + ### Documentation - Achieved **100% docstring coverage** (excluding `__magic__` and private `_methods`) (@atrabattoni). - Improved *User Guide* index (@atrabattoni).