diff --git a/pynapple/core/interval_set.py b/pynapple/core/interval_set.py index c8e5ec02..4403c177 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 @@ -468,6 +470,33 @@ def __getitem__(self, key): # 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) and not duplicated. + if not all(x < y for x, y in zip(key, key[1:])): + key = sorted(list(set(key))) + warnings.warn( + "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, + ) + + elif isinstance(key, slice): + 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( + "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, + ) + if isinstance(key, tuple): if len(key) == 2: # any 2D indexing will only act on start and end values 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 diff --git a/tests/test_interval_set.py b/tests/test_interval_set.py index 6880b0b8..eeb7407a 100644 --- a/tests/test_interval_set.py +++ b/tests/test_interval_set.py @@ -224,6 +224,38 @@ def test_get_iset(): ) +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 or duplicate index"): + 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) end = np.array([5, 15, 20], dtype=np.float64)