From 8c3bb76e3da060c330e92ec2ccf0ed6376946d24 Mon Sep 17 00:00:00 2001 From: Heberto Mayorquin Date: Fri, 2 Oct 2026 14:32:25 -0600 Subject: [PATCH] Add NeuroExplorerSortingExtractor --- doc/api.rst | 1 + doc/modules/extractors.rst | 1 + .../extractors/neoextractors/__init__.py | 10 ++- .../extractors/neoextractors/neuroexplorer.py | 68 ++++++++++++++++++- .../extractors/tests/test_neoextractors.py | 10 +++ 5 files changed, 88 insertions(+), 2 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 335bc186aa..c0ace59d3b 100755 --- a/doc/api.rst +++ b/doc/api.rst @@ -111,6 +111,7 @@ NEO-based .. autofunction:: read_neuralynx .. autofunction:: read_neuralynx_sorting .. autofunction:: read_neuroexplorer + .. autofunction:: read_neuroexplorer_sorting .. autofunction:: read_neuroscope .. autofunction:: read_nix .. autofunction:: read_openephys diff --git a/doc/modules/extractors.rst b/doc/modules/extractors.rst index 89ea3dbc06..3179da863f 100644 --- a/doc/modules/extractors.rst +++ b/doc/modules/extractors.rst @@ -175,6 +175,7 @@ For sorted data formats, we currently support: * **Neurodata Without Borders** :py:func:`~spikeinterface.extractors.read_nwb_sorting()` * **Neuroscope** :py:func:`~spikeinterface.extractors.read_neuroscope_sorting()` * **Neuralynx spikes** :py:func:`~spikeinterface.extractors.read_neuralynx_sorting()` +* **NeuroExplorer spikes** :py:func:`~spikeinterface.extractors.read_neuroexplorer_sorting()` * **NPZ (created by SpikeInterface)** :py:func:`~spikeinterface.core.read_npz_sorting()` * **Plexon spikes** :py:func:`~spikeinterface.extractors.read_plexon_sorting()` * **Plexon 2 spikes** :py:func:`~spikeinterface.extractors.read_plexon2_sorting()` diff --git a/src/spikeinterface/extractors/neoextractors/__init__.py b/src/spikeinterface/extractors/neoextractors/__init__.py index a90e0954f3..4f04d9db0b 100644 --- a/src/spikeinterface/extractors/neoextractors/__init__.py +++ b/src/spikeinterface/extractors/neoextractors/__init__.py @@ -18,7 +18,12 @@ read_neuroscope_sorting, read_neuroscope, ) -from .neuroexplorer import NeuroExplorerRecordingExtractor, read_neuroexplorer +from .neuroexplorer import ( + NeuroExplorerRecordingExtractor, + NeuroExplorerSortingExtractor, + read_neuroexplorer, + read_neuroexplorer_sorting, +) from .nix import NixRecordingExtractor, read_nix from .openephys import ( OpenEphysLegacyRecordingExtractor, @@ -79,6 +84,9 @@ PlexonSortingExtractor: dict(wrapper_string="read_plexon_sorting", wrapper_class=read_plexon_sorting), Plexon2SortingExtractor: dict(wrapper_string="read_plexon2_sorting", wrapper_class=read_plexon2_sorting), NeuroScopeSortingExtractor: dict(wrapper_string="read_neuroscope_sorting", wrapper_class=read_neuroscope_sorting), + NeuroExplorerSortingExtractor: dict( + wrapper_string="read_neuroexplorer_sorting", wrapper_class=read_neuroexplorer_sorting + ), } neo_event_extractors_dict = { diff --git a/src/spikeinterface/extractors/neoextractors/neuroexplorer.py b/src/spikeinterface/extractors/neoextractors/neuroexplorer.py index 8a076264ec..d00995aeb7 100644 --- a/src/spikeinterface/extractors/neoextractors/neuroexplorer.py +++ b/src/spikeinterface/extractors/neoextractors/neuroexplorer.py @@ -1,8 +1,14 @@ from pathlib import Path +from spikeinterface.core import BaseSorting from spikeinterface.core.core_tools import define_function_from_class -from .neobaseextractor import NeoBaseRecordingExtractor +from .neobaseextractor import ( + NeoBaseRecordingExtractor, + NeoBaseSortingExtractor, + NeoSortingSegment, + _NeoBaseExtractor, +) class NeuroExplorerRecordingExtractor(NeoBaseRecordingExtractor): @@ -72,3 +78,63 @@ def map_to_neo_kwargs(cls, file_path): read_neuroexplorer = define_function_from_class(source_class=NeuroExplorerRecordingExtractor, name="read_neuroexplorer") + + +class NeuroExplorerSortingExtractor(NeoBaseSortingExtractor): + """ + Class for reading the sorted units of NEX (NeuroExplorer data format) files. + + Based on :py:class:`neo.rawio.NeuroExplorerRawIO` + + Units are the neuron variables of the file and the unit ids are their names. Waveform variables + (for example the ``_wf`` and ``_template`` companions of each unit in Plexon exports) repeat + the spike times of a neuron variable or hold a mean waveform, so they are not units. They + remain available through ``neo_reader``. + + Parameters + ---------- + file_path : str | Path + The file path to load the sorting from. + """ + + NeoRawIOClass = "NeuroExplorerRawIO" + neo_returns_frames = True + + def __init__(self, file_path): + neo_kwargs = self.map_to_neo_kwargs(file_path) + _NeoBaseExtractor.__init__(self, block_index=None, **neo_kwargs) + + # Only neuron variables are units: they are the spike channels without waveforms + spike_channels = self.neo_reader.header["spike_channels"] + unit_ids = spike_channels["name"][spike_channels["wf_sampling_rate"] == 0] + # Spike timestamps are ticks of the file's global clock + sampling_frequency = self.neo_reader.global_header["freq"] + BaseSorting.__init__(self, sampling_frequency, unit_ids) + + sorting_segment = NeuroExplorerSortingSegment( + neo_reader=self.neo_reader, + block_index=self.block_index, + segment_index=0, + t_start=None, + sampling_frequency=sampling_frequency, + neo_returns_frames=self.neo_returns_frames, + ) + self.add_sorting_segment(sorting_segment) + self._kwargs = {"file_path": str(Path(file_path).absolute())} + + @classmethod + def map_to_neo_kwargs(cls, file_path): + neo_kwargs = {"filename": str(file_path)} + return neo_kwargs + + +class NeuroExplorerSortingSegment(NeoSortingSegment): + def map_from_unit_id_to_spike_channel_index(self, unit_id): + # Units are a subset of the spike channels, so the position of a unit id is not its spike channel index + spike_channel_names = list(self.neo_reader.header["spike_channels"]["name"]) + return spike_channel_names.index(unit_id) + + +read_neuroexplorer_sorting = define_function_from_class( + source_class=NeuroExplorerSortingExtractor, name="read_neuroexplorer_sorting" +) diff --git a/src/spikeinterface/extractors/tests/test_neoextractors.py b/src/spikeinterface/extractors/tests/test_neoextractors.py index 7522218d52..64b597d68a 100644 --- a/src/spikeinterface/extractors/tests/test_neoextractors.py +++ b/src/spikeinterface/extractors/tests/test_neoextractors.py @@ -19,6 +19,7 @@ IntanRecordingExtractor, NeuroScopeRecordingExtractor, NeuroExplorerRecordingExtractor, + NeuroExplorerSortingExtractor, NeuroScopeSortingExtractor, NeuroNexusRecordingExtractor, PlexonRecordingExtractor, @@ -270,6 +271,15 @@ class NeuroExplorerRecordingTest(RecordingCommonTestSuite, unittest.TestCase): ] +class NeuroExplorerSortingTest(SortingCommonTestSuite, unittest.TestCase): + ExtractorClass = NeuroExplorerSortingExtractor + downloads = ["neuroexplorer"] + entities = [ + "neuroexplorer/File_neuroexplorer_1.nex", + "neuroexplorer/File_neuroexplorer_2.nex", + ] + + class NeuroScopeSortingTest(SortingCommonTestSuite, unittest.TestCase): ExtractorClass = NeuroScopeSortingExtractor downloads = ["neuroscope"]