Repository: SpikeInterface/spikeinterface (python)

Issue:
Is it possible to access original data through preprocesed `BaseRecording` instance
The pipeline
```
Binary File -> Preprocessing -(1)-> Sorting -(2)-> Wave Extractor -(3)-> Neuron feature assessment. 
```
The last class gets `BaseRecording` after (1) and uses `get_traces` to compute a spike "fingerprint". Unfortunately, preprocessing removes something we are looking for. Would it be possible to `get_traces` from the original Binary File without reloading it and creating an additional `BaseRecording` object?

In other words, is there something like that
```python
# preproc is BaseRecording (1)
# which is pointing to the filtered and zeroed data
fingerprint = array([
        preproc.original_data.get_traces(
            start_frame   = sp+frmstart,
            end_frame     = sp+frmend).T
        for sp in sp_train
        ])
```

Pull request description:
Implement `get_parent()` function
Fixes #2596

This is super useful for example to plot different processing layers!

Patch:
diff --git a/src/spikeinterface/core/base.py b/src/spikeinterface/core/base.py
index 80341811b..f234d77da 100644
--- a/src/spikeinterface/core/base.py
+++ b/src/spikeinterface/core/base.py
@@ -82,6 +82,10 @@ class BaseExtractor:
         # This is implemented in BaseRecording or BaseSorting
         raise NotImplementedError
 
+    def get_parent(self) -> Optional[BaseExtractor]:
+        """Returns parent object if it exists, otherwise None"""
+        return getattr(self, "_parent", None)
+
     def _check_segment_index(self, segment_index: Optional[int] = None) -> int:
         if segment_index is None:
             if self.get_num_segments() == 1:
diff --git a/src/spikeinterface/core/channelsaggregationrecording.py b/src/spikeinterface/core/channelsaggregationrecording.py
index bb29ab985..3b764725a 100644
--- a/src/spikeinterface/core/channelsaggregationrecording.py
+++ b/src/spikeinterface/core/channelsaggregationrecording.py
@@ -93,6 +93,10 @@ class ChannelsAggregationRecording(BaseRecording):
         self._recordings = recording_list
         self._kwargs = {"recording_list": [rec for rec in recording_list], "renamed_channel_ids": renamed_channel_ids}
 
+    @property
+    def recordings(self):
+        return self._recordings
+
 
 class ChannelsAggregationRecordingSegment(BaseRecordingSegment):
     """
diff --git a/src/spikeinterface/core/channelslice.py b/src/spikeinterface/core/channelslice.py
index 3fb3ab106..dbf067294 100644
--- a/src/spikeinterface/core/channelslice.py
+++ b/src/spikeinterface/core/channelslice.py
@@ -60,6 +60,7 @@ class ChannelSliceRecording(BaseRecording):
 
         # copy annotation and properties
         parent_recording.copy_metadata(self, only_main=False, ids=self._channel_ids)
+        self._parent = parent_recording
 
         # change the wiring of the probe
         contact_vector = self.get_property("contact_vector")
diff --git a/src/spikeinterface/core/frameslicerecording.py b/src/spikeinterface/core/frameslicerecording.py
index cc899650d..533328ad4 100644
--- a/src/spikeinterface/core/frameslicerecording.py
+++ b/src/spikeinterface/core/frameslicerecording.py
@@ -59,6 +59,7 @@ class FrameSliceRecording(BaseRecording):
 
         # copy properties and annotations
         parent_recording.copy_metadata(self)
+        self._parent = parent_recording
 
         # update dump dict
         self._kwargs = {
diff --git a/src/spikeinterface/core/frameslicesorting.py b/src/spikeinterface/core/frameslicesorting.py
index 4d6121cc9..87280d983 100644
--- a/src/spikeinterface/core/frameslicesorting.py
+++ b/src/spikeinterface/core/frameslicesorting.py
@@ -82,6 +82,7 @@ class FrameSliceSorting(BaseSorting):
 
         # copy properties and annotations
         parent_sorting.copy_metadata(self)
+        self._parent = parent_sorting
 
         if parent_sorting.has_recording():
             self.register_recording(parent_sorting._recording.frame_slice(start_frame=start_frame, end_frame=end_frame))
diff --git a/src/spikeinterface/core/unitsselectionsorting.py b/src/spikeinterface/core/unitsselectionsorting.py
index 5748d11f4..14454b2e2 100644
--- a/src/spikeinterface/core/unitsselectionsorting.py
+++ b/src/spikeinterface/core/unitsselectionsorting.py
@@ -40,6 +40,7 @@ class UnitsSelectionSorting(BaseSorting):
             self.add_sorting_segment(sub_segment)
 
         parent_sorting.copy_metadata(self, only_main=False, ids=self._unit_ids)
+        self._parent = parent_sorting
 
         if parent_sorting.has_recording():
             self.register_recording(parent_sorting._recording)
diff --git a/src/spikeinterface/curation/remove_duplicated_spikes.py b/src/spikeinterface/curation/remove_duplicated_spikes.py
index 93d310af3..9e33f7bcb 100644
--- a/src/spikeinterface/curation/remove_duplicated_spikes.py
+++ b/src/spikeinterface/curation/remove_duplicated_spikes.py
@@ -45,6 +45,7 @@ class RemoveDuplicatedSpikesSorting(BaseSorting):
             )
 
         sorting.copy_metadata(self, only_main=False)
+        self._parent = sorting
         if sorting.has_recording():
             self.register_recording(sorting._recording)
 
diff --git a/src/spikeinterface/curation/remove_excess_spikes.py b/src/spikeinterface/curation/remove_excess_spikes.py
index 9c37a48ba..b9f00212e 100644
--- a/src/spikeinterface/curation/remove_excess_spikes.py
+++ b/src/spikeinterface/curation/remove_excess_spikes.py
@@ -41,6 +41,7 @@ class RemoveExcessSpikesSorting(BaseSorting):
             )
 
         sorting.copy_metadata(self, only_main=False)
+        self._parent = sorting
         if sorting.has_recording():
             self.register_recording(sorting._recording)
 
diff --git a/src/spikeinterface/postprocessing/alignsorting.py b/src/spikeinterface/postprocessing/alignsorting.py
index 0ff76aec8..4f031eb63 100644
--- a/src/spikeinterface/postprocessing/alignsorting.py
+++ b/src/spikeinterface/postprocessing/alignsorting.py
@@ -35,6 +35,7 @@ class AlignSortingExtractor(BaseSorting):
             self.add_sorting_segment(AlignSortingSegment(segment, unit_peak_shifts))
 
         sorting.copy_metadata(self, only_main=False)
+        self._parent = sorting
         if sorting.has_recording():
             self.register_recording(sorting._recording)
 
diff --git a/src/spikeinterface/preprocessing/align_snippets.py b/src/spikeinterface/preprocessing/align_snippets.py
index 04f7ff1d7..c37f8f2a9 100644
--- a/src/spikeinterface/preprocessing/align_snippets.py
+++ b/src/spikeinterface/preprocessing/align_snippets.py
@@ -27,6 +27,7 @@ class AlignSnippets(BaseSnippets):
         assert self.snippet_len >= new_nbefore + new_nafter, "snippet_len smaller than new_nbefore+new_nafter"
 
         snippets.copy_metadata(self, only_main=False, ids=None)
+        self._parent = snippets
 
         for i in range(snippets.get_num_segments()):
             self.add_snippets_segment(
diff --git a/src/spikeinterface/preprocessing/basepreprocessor.py b/src/spikeinterface/preprocessing/basepreprocessor.py
index d50e4a914..106f5e2d9 100644
--- a/src/spikeinterface/preprocessing/basepreprocessor.py
+++ b/src/spikeinterface/preprocessing/basepreprocessor.py
@@ -23,6 +23,7 @@ class BasePreprocessor(BaseRecording):
 
         BaseRecording.__init__(self, sampling_frequency, _channel_ids, dtype)
         recording.copy_metadata(self, only_main=False, ids=channel_ids)
+        self._parent = recording
 
         # self._kwargs have to be handled in subclass
 
diff --git a/src/spikeinterface/preprocessing/zero_channel_pad.py b/src/spikeinterface/preprocessing/zero_channel_pad.py
index ca5654247..a75bebbb8 100644
--- a/src/spikeinterface/preprocessing/zero_channel_pad.py
+++ b/src/spikeinterface/preprocessing/zero_channel_pad.py
@@ -188,6 +188,7 @@ class ZeroChannelPaddedRecording(BaseRecording):
 
         # only copy relevant metadata and properties
         parent_recording.copy_metadata(self, only_main=True)
+        self._parent = parent_recording
         prop_keys = parent_recording.get_property_keys()
 
         for k in prop_keys:


Test patch:
diff --git a/src/spikeinterface/core/tests/test_channelslicerecording.py b/src/spikeinterface/core/tests/test_channelslicerecording.py
index 87481a2a7..e2e4dfdb2 100644
--- a/src/spikeinterface/core/tests/test_channelslicerecording.py
+++ b/src/spikeinterface/core/tests/test_channelslicerecording.py
@@ -42,6 +42,7 @@ def test_ChannelSliceRecording():
     assert traces.shape[1] == 2
     traces = rec_sliced.get_traces(segment_index=1, channel_ids=[2, 0])
     assert traces.shape[1] == 2
+    assert rec_sliced.get_parent() == rec
 
     assert np.allclose(rec_sliced.get_times(0), rec.get_times(0))
 
diff --git a/src/spikeinterface/core/tests/test_frameslicerecording.py b/src/spikeinterface/core/tests/test_frameslicerecording.py
index 9664f3184..fa0b76862 100644
--- a/src/spikeinterface/core/tests/test_frameslicerecording.py
+++ b/src/spikeinterface/core/tests/test_frameslicerecording.py
@@ -17,6 +17,7 @@ def test_FrameSliceRecording():
     assert np.array_equal(traces[:, 0], np.arange(0, 1000, dtype="float64"))
     sub_times0 = sub_rec.get_times(0)
     assert np.allclose(times0, sub_times0)
+    assert sub_rec.get_parent() == rec
 
     sub_rec = rec.frame_slice(None, 10)
     assert sub_rec.get_num_samples(0) == 10
diff --git a/src/spikeinterface/core/tests/test_frameslicesorting.py b/src/spikeinterface/core/tests/test_frameslicesorting.py
index e404cfb1b..7c1ad0ae3 100644
--- a/src/spikeinterface/core/tests/test_frameslicesorting.py
+++ b/src/spikeinterface/core/tests/test_frameslicesorting.py
@@ -42,6 +42,7 @@ def test_FrameSliceSorting():
     assert sub_sorting._recording.get_total_samples() == nsamp
     sub_sorting_norec = sorting.frame_slice(start_frame, end_frame)
     assert np.array_equal(sub_sorting_norec.get_unit_spike_train("0"), unit_0_train)
+    assert sub_sorting.get_parent() == sorting
 
     start_frame, end_frame = None, mid_frame
     sub_sorting = sorting.frame_slice(start_frame, end_frame)
diff --git a/src/spikeinterface/core/tests/test_unitsselectionsorting.py b/src/spikeinterface/core/tests/test_unitsselectionsorting.py
index e86cafea4..1e72b0ab2 100644
--- a/src/spikeinterface/core/tests/test_unitsselectionsorting.py
+++ b/src/spikeinterface/core/tests/test_unitsselectionsorting.py
@@ -12,6 +12,7 @@ def test_basic_functions():
 
     sorting2 = UnitsSelectionSorting(sorting, unit_ids=[0, 2])
     assert np.array_equal(sorting2.unit_ids, [0, 2])
+    assert sorting2.get_parent() == sorting
 
     sorting3 = UnitsSelectionSorting(sorting, unit_ids=[0, 2], renamed_unit_ids=["a", "b"])
     assert np.array_equal(sorting3.unit_ids, ["a", "b"])