From 12a5324e26f82437ae26a26d72831eed6babdaed Mon Sep 17 00:00:00 2001 From: chrishalcrow Date: Wed, 14 Oct 2026 16:50:49 +0100 Subject: [PATCH 1/5] allow for analyzer with rec attributes to get main channel indices --- src/spikeinterface/core/sortinganalyzer.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/spikeinterface/core/sortinganalyzer.py b/src/spikeinterface/core/sortinganalyzer.py index b702237445..234a381b99 100644 --- a/src/spikeinterface/core/sortinganalyzer.py +++ b/src/spikeinterface/core/sortinganalyzer.py @@ -817,8 +817,16 @@ def _handle_backward_compatibility_settings_pre_init(cls, settings: dict[str, An def main_channel_indices(self): if self._main_channel_indices is None: sorting_main_channel_ids = self.get_sorting_property("main_channel_id") - if sorting_main_channel_ids is not None and self.has_recording(): - main_channel_indices = self.recording.ids_to_indices(sorting_main_channel_ids) + if sorting_main_channel_ids is not None and ( + self.has_recording() or self.rec_attributes.get("channel_ids") + ): + if self.has_recording(): + main_channel_indices = self.recording.ids_to_indices(sorting_main_channel_ids) + elif self.rec_attributes.get("channel_ids"): + channel_ids = np.array(self.rec_attributes.get("channel_ids")) + main_channel_indices = np.array( + [list(channel_ids).index(id) for id in sorting_main_channel_ids], dtype=int + ) self._main_channel_indices = main_channel_indices else: self._main_channel_indices = self._compute_main_channel_backwards_compatibility() From d1dd3804e2000c6bc11fa8f17eaa60d83dbb1ca8 Mon Sep 17 00:00:00 2001 From: Chris Halcrow <57948917+chrishalcrow@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:34:56 +0100 Subject: [PATCH 2/5] Apply suggestion from @chrishalcrow --- src/spikeinterface/core/sortinganalyzer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spikeinterface/core/sortinganalyzer.py b/src/spikeinterface/core/sortinganalyzer.py index 234a381b99..fb3ce51965 100644 --- a/src/spikeinterface/core/sortinganalyzer.py +++ b/src/spikeinterface/core/sortinganalyzer.py @@ -818,7 +818,7 @@ def main_channel_indices(self): if self._main_channel_indices is None: sorting_main_channel_ids = self.get_sorting_property("main_channel_id") if sorting_main_channel_ids is not None and ( - self.has_recording() or self.rec_attributes.get("channel_ids") + self.has_recording() or self.rec_attributes ): if self.has_recording(): main_channel_indices = self.recording.ids_to_indices(sorting_main_channel_ids) From cd28b974d0547618cd6b691aa55c6c92c82f43e0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:35:29 +0000 Subject: [PATCH 3/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/spikeinterface/core/sortinganalyzer.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/spikeinterface/core/sortinganalyzer.py b/src/spikeinterface/core/sortinganalyzer.py index fb3ce51965..d33c797963 100644 --- a/src/spikeinterface/core/sortinganalyzer.py +++ b/src/spikeinterface/core/sortinganalyzer.py @@ -817,9 +817,7 @@ def _handle_backward_compatibility_settings_pre_init(cls, settings: dict[str, An def main_channel_indices(self): if self._main_channel_indices is None: sorting_main_channel_ids = self.get_sorting_property("main_channel_id") - if sorting_main_channel_ids is not None and ( - self.has_recording() or self.rec_attributes - ): + if sorting_main_channel_ids is not None and (self.has_recording() or self.rec_attributes): if self.has_recording(): main_channel_indices = self.recording.ids_to_indices(sorting_main_channel_ids) elif self.rec_attributes.get("channel_ids"): From 58f1ed095afd06f17e1adf6adf7ceed2e1cf7df7 Mon Sep 17 00:00:00 2001 From: chrishalcrow Date: Tue, 27 Oct 2026 09:52:53 +0000 Subject: [PATCH 4/5] Handle case that channel_ids not in attributes --- src/spikeinterface/core/sortinganalyzer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/spikeinterface/core/sortinganalyzer.py b/src/spikeinterface/core/sortinganalyzer.py index e40758cef6..1873b3134b 100644 --- a/src/spikeinterface/core/sortinganalyzer.py +++ b/src/spikeinterface/core/sortinganalyzer.py @@ -833,11 +833,13 @@ def main_channel_indices(self): if sorting_main_channel_ids is not None and (self.has_recording() or self.rec_attributes): if self.has_recording(): main_channel_indices = self.recording.ids_to_indices(sorting_main_channel_ids) - elif self.rec_attributes.get("channel_ids"): + elif self.rec_attributes.get("channel_ids") is not None: channel_ids = np.array(self.rec_attributes.get("channel_ids")) main_channel_indices = np.array( [list(channel_ids).index(id) for id in sorting_main_channel_ids], dtype=int ) + else: + main_channel_indices = None self._main_channel_indices = main_channel_indices else: self._main_channel_indices = self._compute_main_channel_backwards_compatibility() From 27a6d8b04e1d9f47845c5e7c53ccd290f3d915d7 Mon Sep 17 00:00:00 2001 From: chrishalcrow Date: Tue, 27 Oct 2026 09:53:16 +0000 Subject: [PATCH 5/5] Change tests to be sorting with no main channel ids instead of recordingless --- .../core/tests/test_sortinganalyzer.py | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/spikeinterface/core/tests/test_sortinganalyzer.py b/src/spikeinterface/core/tests/test_sortinganalyzer.py index 9372210400..50eb925444 100644 --- a/src/spikeinterface/core/tests/test_sortinganalyzer.py +++ b/src/spikeinterface/core/tests/test_sortinganalyzer.py @@ -1181,10 +1181,10 @@ def test_select_channels_independent(dataset): assert np.all(sa_one.get_channel_locations() == sa_two.get_channel_locations()[shuffle_order]) -def test_main_channel_from_templates_dense_recordingless(tmp_path): - """When a dense analyzer has a `templates` extension but no attached recording, `get_main_channels` - recovers each unit's main channel from the templates (its peak channel) and reports it consistently - both as a positional index and as a channel id. +def test_main_channel_from_templates_dense_no_sorting_main_channel_ids(tmp_path): + """When a dense analyzer has a `templates` extension but the sorting has no main channel + ids, `get_main_channels, recovers each unit's main channel from the templates (its peak + channel) and reports it consistently both as a positional index and as a channel id. """ recording, sorting = generate_ground_truth_recording( num_channels=8, @@ -1210,18 +1210,16 @@ def test_main_channel_from_templates_dense_recordingless(tmp_path): expected_main_channel_ids = [unit0_main_channel_id, unit1_main_channel_id] folder = tmp_path / "analyzer_recordingless" - analyzer = create_sorting_analyzer(sorting, recording, format="binary_folder", folder=folder, sparse=False) + analyzer = create_sorting_analyzer(sorting, recording, format="memory", folder=folder, sparse=False) analyzer.compute(["random_spikes", "templates"]) - # simulate the recording being unavailable at load time by removing its serialized copy on disk - for file_ext in ("json", "pickle"): - recording_file = folder / f"recording.{file_ext}" - if recording_file.exists(): - recording_file.unlink() + analyzer._recording = None + analyzer.sorting._properties.pop("main_channel_id") + + analyzer.save_as(format="binary_folder", folder=folder) reloaded_analyzer = load_sorting_analyzer(folder) - assert not reloaded_analyzer.has_recording() - assert reloaded_analyzer.has_extension("templates") + assert "main_channel_id" not in reloaded_analyzer.get_sorting_property_keys() # craft a clear negative peak on each unit's main channel templates = reloaded_analyzer.get_extension("templates") @@ -1238,8 +1236,8 @@ def test_main_channel_from_templates_dense_recordingless(tmp_path): assert np.array_equal(recovered_main_channel_ids, expected_main_channel_ids) -def test_main_channel_from_templates_sparse_recordingless(): - """When a sparse analyzer has a `templates` extension but no attached recording, `get_main_channels` +def test_main_channel_from_templates_sparse_no_sorting_main_channel_ids(): + """When a sparse analyzer has a `templates` extension but sorting has no main channel ids, `get_main_channels` reports each unit's main channel as its template peak channel, not the geometric centroid of the unit's sparse channel set. """ @@ -1293,8 +1291,8 @@ def test_main_channel_from_templates_sparse_recordingless(): templates.data["average"] = average # recordingless, and force the backwards-compatibility recompute - analyzer._recording = None analyzer._main_channel_indices = None + analyzer.sorting._properties.pop("main_channel_id") # must be the template peak channels, not the geometric centroids ("c", "f" / indices [2, 5]) recovered_main_channel_indices = np.asarray(analyzer.get_main_channels(outputs="index"))