From 023d457dd209c6d3e55e4465647b7b2f6029f8b9 Mon Sep 17 00:00:00 2001 From: lilytong0919 <106998357+lilytong0919@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:04:40 -0400 Subject: [PATCH 1/7] Update __getitem__() method in IntervalSet object to sort index, an attempt to preserve metadata. This will prevent metadata to be dropped when indexing is unsorted list or a reverse slice. Did not any test, don't seem necessary, but could add some if needed. Still need to do: Did not make any modification to indexing with tuple, but planed to. The logic seems a little different there. Need to think about it. --- pynapple/core/interval_set.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pynapple/core/interval_set.py b/pynapple/core/interval_set.py index c8e5ec02..8b008316 100644 --- a/pynapple/core/interval_set.py +++ b/pynapple/core/interval_set.py @@ -467,6 +467,25 @@ def __getitem__(self, key): # self[[*str]] # only works for list of metadata columns return _MetadataMixin.__getitem__(self, key) + + elif isinstance(key, list) and all(isinstance(x, int) for x in key): + # check if list is sorted (ascending) + if not all(x < y for x, y in zip(key, key[1:])): + key = sorted(key) + warnings.warn("Recieved unsorted index, this is sorted to preserve the invariant that " + "nap.IntervalSet remains ordered. This differs from standard NumPy/Pandas " \ + "indexing semantics as index order is not preserved.", + UserWarning) + + elif isinstance(key,slice): + if key.step is None: + pass # prevent None step to go into next condition. No action needed + elif key.step < 0: + warnings.warn("Recieved descending slice, this is reversed to preserve the invariant that " + "nap.IntervalSet remains ordered. This differs from standard NumPy/Pandas " \ + "indexing semantics as index order is not preserved.", + UserWarning) + key = slice(key.stop + 1, key.start + 1, -key.step) if isinstance(key, tuple): if len(key) == 2: From 1411327caf3b1dfffe1061c9533b38dfbc0cef1d Mon Sep 17 00:00:00 2001 From: lilytong0919 <106998357+lilytong0919@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:35:30 -0400 Subject: [PATCH 2/7] Edit logic to better fit with original function Although this passed all test as an elif block following the condition for given column index. Since I am writing a slightly different behavior (not returning but modify the key (sort or reverse slice) so in the final else block the metadata will not be dropped due to unsorted input, maybe it is better that this is a separate block from above column index. Shouldn't make an actual difference since the column indexing will do early return. --- pynapple/core/interval_set.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pynapple/core/interval_set.py b/pynapple/core/interval_set.py index 8b008316..ce01bc47 100644 --- a/pynapple/core/interval_set.py +++ b/pynapple/core/interval_set.py @@ -468,7 +468,9 @@ def __getitem__(self, key): # only works for list of metadata columns return _MetadataMixin.__getitem__(self, key) - elif isinstance(key, list) and all(isinstance(x, int) for x in key): + # A separate if-elif block to reorder key if given row style index of type + # list[int] or slice that is not in ascending order. + if isinstance(key, list) and all(isinstance(x, int) for x in key): # check if list is sorted (ascending) if not all(x < y for x, y in zip(key, key[1:])): key = sorted(key) From 20311cd9f67ba0549db5d271723b1cea1b119884 Mon Sep 17 00:00:00 2001 From: lilytong0919 <106998357+lilytong0919@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:24:14 -0400 Subject: [PATCH 3/7] Formatting changes --- pynapple/core/interval_set.py | 32 +++++++++++++++++++------------- pynapple/io/loader.py | 4 +++- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/pynapple/core/interval_set.py b/pynapple/core/interval_set.py index ce01bc47..0605b4fd 100644 --- a/pynapple/core/interval_set.py +++ b/pynapple/core/interval_set.py @@ -192,7 +192,9 @@ def __init__( start = start.start.astype(np.float64) elif isinstance(start, pd.DataFrame): - assert "start" in start.columns and "end" in start.columns, """ + assert ( + "start" in start.columns and "end" in start.columns + ), """ DataFrame must contain columns name "start" and "end" for start and end times. """ # try sorting the DataFrame by start times, preserving its end pair, as an effort to preserve metadata @@ -467,26 +469,30 @@ def __getitem__(self, key): # self[[*str]] # only works for list of metadata columns return _MetadataMixin.__getitem__(self, key) - + # A separate if-elif block to reorder key if given row style index of type # list[int] or slice that is not in ascending order. if isinstance(key, list) and all(isinstance(x, int) for x in key): # check if list is sorted (ascending) if not all(x < y for x, y in zip(key, key[1:])): key = sorted(key) - warnings.warn("Recieved unsorted index, this is sorted to preserve the invariant that " - "nap.IntervalSet remains ordered. This differs from standard NumPy/Pandas " \ - "indexing semantics as index order is not preserved.", - UserWarning) - - elif isinstance(key,slice): + warnings.warn( + "Recieved unsorted index, this is sorted to preserve the invariant that " + "nap.IntervalSet remains ordered. This differs from standard NumPy/Pandas " + "indexing semantics as index order is not preserved.", + UserWarning, + ) + + elif isinstance(key, slice): if key.step is None: - pass # prevent None step to go into next condition. No action needed + pass # prevent None step to go into next condition. No action needed elif key.step < 0: - warnings.warn("Recieved descending slice, this is reversed to preserve the invariant that " - "nap.IntervalSet remains ordered. This differs from standard NumPy/Pandas " \ - "indexing semantics as index order is not preserved.", - UserWarning) + warnings.warn( + "Recieved descending slice, this is reversed to preserve the invariant that " + "nap.IntervalSet remains ordered. This differs from standard NumPy/Pandas " + "indexing semantics as index order is not preserved.", + UserWarning, + ) key = slice(key.stop + 1, key.start + 1, -key.step) if isinstance(key, tuple): diff --git a/pynapple/io/loader.py b/pynapple/io/loader.py index d8877c4c..371d036c 100644 --- a/pynapple/io/loader.py +++ b/pynapple/io/loader.py @@ -41,7 +41,9 @@ def get_error_text(path): A more advanced project for creating NWB files is neuroconv: https://neuroconv.readthedocs.io/en/main/ - """.format(path) + """.format( + path + ) error_txt = "\n" + border + "\n" + txt1 + "\n" + border return error_txt From 24f8eb7d2cb929b15de500c352345daf145d800a Mon Sep 17 00:00:00 2001 From: lilytong0919 <106998357+lilytong0919@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:04:17 -0400 Subject: [PATCH 4/7] Changed handling of descending slice The previous way will not handle descending slice with step < -1 correctly. Now instead of modify the slice parameters I sort the recreated a sorted list of integers from the slice parameters using list comprehension. Now that I realized this.. Maybe I do need to add test... --- pynapple/core/interval_set.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pynapple/core/interval_set.py b/pynapple/core/interval_set.py index 0605b4fd..96b6a8eb 100644 --- a/pynapple/core/interval_set.py +++ b/pynapple/core/interval_set.py @@ -484,16 +484,18 @@ def __getitem__(self, key): ) elif isinstance(key, slice): - if key.step is None: - pass # prevent None step to go into next condition. No action needed - elif key.step < 0: + if key.step is None or key.step > 0: + pass # positive or defautl step no action needed + else: + # if slice is descending, compute the actual index and reorder it + # to be in ascending order. + key = sorted([i for i in range(*key.indices(self.shape[0]))]) warnings.warn( "Recieved descending slice, this is reversed to preserve the invariant that " "nap.IntervalSet remains ordered. This differs from standard NumPy/Pandas " "indexing semantics as index order is not preserved.", UserWarning, ) - key = slice(key.stop + 1, key.start + 1, -key.step) if isinstance(key, tuple): if len(key) == 2: From 31855e798625b35c9a72aaa1254a93d4735cbc04 Mon Sep 17 00:00:00 2001 From: lilytong0919 <106998357+lilytong0919@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:53:05 -0400 Subject: [PATCH 5/7] Add removal of duplicate index and added tests Modified reorder of list keys to remove duplicate index. Also added test case to assert output returns the values intended by the slice (except being reversed). Added testing that metadata is properly handled when handling reverse index. --- pynapple/core/interval_set.py | 7 ++++--- tests/test_interval_set.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/pynapple/core/interval_set.py b/pynapple/core/interval_set.py index 96b6a8eb..ea3d1b64 100644 --- a/pynapple/core/interval_set.py +++ b/pynapple/core/interval_set.py @@ -473,11 +473,12 @@ def __getitem__(self, key): # A separate if-elif block to reorder key if given row style index of type # list[int] or slice that is not in ascending order. if isinstance(key, list) and all(isinstance(x, int) for x in key): - # check if list is sorted (ascending) + # check if list is sorted (ascending) and not duplicated. if not all(x < y for x, y in zip(key, key[1:])): - key = sorted(key) + key = sorted(list(set(key))) warnings.warn( - "Recieved unsorted index, this is sorted to preserve the invariant that " + "Recieved unsorted index or index with duplicates," + "this is sorted to preserve the invariant that " "nap.IntervalSet remains ordered. This differs from standard NumPy/Pandas " "indexing semantics as index order is not preserved.", UserWarning, diff --git a/tests/test_interval_set.py b/tests/test_interval_set.py index 6880b0b8..1881e37f 100644 --- a/tests/test_interval_set.py +++ b/tests/test_interval_set.py @@ -223,6 +223,34 @@ def test_get_iset(): str(e.value) == "too many indices for IntervalSet: IntervalSet is 2-dimensional" ) +def test_get_iset_non_ascending(): + # Get new ivset with unsorted/descending indices + # Needed a slightly longer test case with metadat for this. + # So I maded a separate test block + start = np.array([0, 10, 16, 21, 26], dtype=np.float64) + end = np.array([5, 15, 20, 25, 30], dtype=np.float64) + metadata = pd.DataFrame({ + "label": ["a", "b", "c", "d", "e"], + "score": [1, 2, 3, 4, 5], + }) + ep = nap.IntervalSet(start=start, end=end,metadata=metadata) + with pytest.warns(UserWarning, match="descending slice"): + ep2 = ep[::-2] + assert isinstance(ep2, nap.IntervalSet) + expected_values = ep.values[::-2][::-1] + np.testing.assert_array_almost_equal(ep2.values, expected_values) + expected_metadata = metadata.iloc[::-2].iloc[::-1].reset_index(drop=True) + pd.testing.assert_frame_equal(ep2.metadata, expected_metadata) + + idx = [0, 4 , 4, 2] + with pytest.warns(UserWarning, match="unsorted index or index with duplicates"): + ep2 = ep[idx] + assert isinstance(ep2, nap.IntervalSet) + expected_indices = sorted(list(set(idx))) + expected_values = ep.values[expected_indices] + np.testing.assert_array_almost_equal(ep2.values, expected_values) + expected_metadata = metadata.iloc[expected_indices].reset_index(drop=True) + pd.testing.assert_frame_equal(ep2.metadata, expected_metadata) def test_get_iset_with_series(): start = np.array([0, 10, 16], dtype=np.float64) From cd521cb5f4039caec39afdb501d373e74b5adf89 Mon Sep 17 00:00:00 2001 From: lilytong0919 <106998357+lilytong0919@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:57:51 -0400 Subject: [PATCH 6/7] Correct spelling and wordings in warning message --- pynapple/core/interval_set.py | 5 ++--- tests/test_interval_set.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pynapple/core/interval_set.py b/pynapple/core/interval_set.py index ea3d1b64..acce6da0 100644 --- a/pynapple/core/interval_set.py +++ b/pynapple/core/interval_set.py @@ -477,8 +477,7 @@ def __getitem__(self, key): if not all(x < y for x, y in zip(key, key[1:])): key = sorted(list(set(key))) warnings.warn( - "Recieved unsorted index or index with duplicates," - "this is sorted to preserve the invariant that " + "Received an unsorted or duplicate index, this is sorted to preserve the invariant that " "nap.IntervalSet remains ordered. This differs from standard NumPy/Pandas " "indexing semantics as index order is not preserved.", UserWarning, @@ -492,7 +491,7 @@ def __getitem__(self, key): # to be in ascending order. key = sorted([i for i in range(*key.indices(self.shape[0]))]) warnings.warn( - "Recieved descending slice, this is reversed to preserve the invariant that " + "Received descending slice, this is reversed to preserve the invariant that " "nap.IntervalSet remains ordered. This differs from standard NumPy/Pandas " "indexing semantics as index order is not preserved.", UserWarning, diff --git a/tests/test_interval_set.py b/tests/test_interval_set.py index 1881e37f..d2749d34 100644 --- a/tests/test_interval_set.py +++ b/tests/test_interval_set.py @@ -243,7 +243,7 @@ def test_get_iset_non_ascending(): pd.testing.assert_frame_equal(ep2.metadata, expected_metadata) idx = [0, 4 , 4, 2] - with pytest.warns(UserWarning, match="unsorted index or index with duplicates"): + with pytest.warns(UserWarning, match="unsorted or duplicate index"): ep2 = ep[idx] assert isinstance(ep2, nap.IntervalSet) expected_indices = sorted(list(set(idx))) From fc9d62eb4568926bb1d523537da40266360bd112 Mon Sep 17 00:00:00 2001 From: lilytong0919 <106998357+lilytong0919@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:18:47 -0400 Subject: [PATCH 7/7] Reformat with black . For some reason can't do tox and cannot pass lint... Lemme try reformatting myself. --- pynapple/core/interval_set.py | 2 +- tests/test_interval_set.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pynapple/core/interval_set.py b/pynapple/core/interval_set.py index acce6da0..4403c177 100644 --- a/pynapple/core/interval_set.py +++ b/pynapple/core/interval_set.py @@ -487,7 +487,7 @@ def __getitem__(self, key): if key.step is None or key.step > 0: pass # positive or defautl step no action needed else: - # if slice is descending, compute the actual index and reorder it + # if slice is descending, compute the actual index and reorder it # to be in ascending order. key = sorted([i for i in range(*key.indices(self.shape[0]))]) warnings.warn( diff --git a/tests/test_interval_set.py b/tests/test_interval_set.py index d2749d34..eeb7407a 100644 --- a/tests/test_interval_set.py +++ b/tests/test_interval_set.py @@ -223,17 +223,20 @@ def test_get_iset(): str(e.value) == "too many indices for IntervalSet: IntervalSet is 2-dimensional" ) + def test_get_iset_non_ascending(): # Get new ivset with unsorted/descending indices # Needed a slightly longer test case with metadat for this. # So I maded a separate test block start = np.array([0, 10, 16, 21, 26], dtype=np.float64) end = np.array([5, 15, 20, 25, 30], dtype=np.float64) - metadata = pd.DataFrame({ - "label": ["a", "b", "c", "d", "e"], - "score": [1, 2, 3, 4, 5], - }) - ep = nap.IntervalSet(start=start, end=end,metadata=metadata) + metadata = pd.DataFrame( + { + "label": ["a", "b", "c", "d", "e"], + "score": [1, 2, 3, 4, 5], + } + ) + ep = nap.IntervalSet(start=start, end=end, metadata=metadata) with pytest.warns(UserWarning, match="descending slice"): ep2 = ep[::-2] assert isinstance(ep2, nap.IntervalSet) @@ -242,7 +245,7 @@ def test_get_iset_non_ascending(): expected_metadata = metadata.iloc[::-2].iloc[::-1].reset_index(drop=True) pd.testing.assert_frame_equal(ep2.metadata, expected_metadata) - idx = [0, 4 , 4, 2] + idx = [0, 4, 4, 2] with pytest.warns(UserWarning, match="unsorted or duplicate index"): ep2 = ep[idx] assert isinstance(ep2, nap.IntervalSet) @@ -252,6 +255,7 @@ def test_get_iset_non_ascending(): expected_metadata = metadata.iloc[expected_indices].reset_index(drop=True) pd.testing.assert_frame_equal(ep2.metadata, expected_metadata) + def test_get_iset_with_series(): start = np.array([0, 10, 16], dtype=np.float64) end = np.array([5, 15, 20], dtype=np.float64)