From 3dc57290dbde0aeaa5048f2301ee75015a93fe26 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Mon, 29 Dec 2025 15:43:44 +0100 Subject: [PATCH 1/6] Test IBL extractors tests failing for PI update --- src/spikeinterface/extractors/tests/test_iblextractors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spikeinterface/extractors/tests/test_iblextractors.py b/src/spikeinterface/extractors/tests/test_iblextractors.py index 972a8e7bb0..56d01e38cf 100644 --- a/src/spikeinterface/extractors/tests/test_iblextractors.py +++ b/src/spikeinterface/extractors/tests/test_iblextractors.py @@ -76,8 +76,8 @@ def test_offsets(self): def test_probe_representation(self): probe = self.recording.get_probe() - expected_probe_representation = "Probe - 384ch - 1shanks" - assert repr(probe) == expected_probe_representation + expected_probe_representation = "Probe - 384ch" + assert expected_probe_representation in repr(probe) def test_property_keys(self): expected_property_keys = [ From 61c317aba92608d9f096a3a374bc3d43e27faaba Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Fri, 6 Mar 2026 10:09:46 -0800 Subject: [PATCH 2/6] Fix OpenEphys tests --- .../extractors/neoextractors/openephys.py | 20 ++++++++++++------- .../extractors/tests/test_neoextractors.py | 3 +++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/spikeinterface/extractors/neoextractors/openephys.py b/src/spikeinterface/extractors/neoextractors/openephys.py index 1c39a1b97c..1d16df534b 100644 --- a/src/spikeinterface/extractors/neoextractors/openephys.py +++ b/src/spikeinterface/extractors/neoextractors/openephys.py @@ -351,13 +351,19 @@ def __init__( # Ensure device channel index corresponds to channel_ids probe_channel_names = probe.contact_annotations.get("channel_name", None) if probe_channel_names is not None and not np.array_equal(probe_channel_names, self.channel_ids): - device_channel_indices = [] - probe_channel_names = list(probe_channel_names) - device_channel_indices = np.zeros(len(self.channel_ids), dtype=int) - for i, ch in enumerate(self.channel_ids): - index_in_probe = probe_channel_names.index(ch) - device_channel_indices[index_in_probe] = i - probe.set_device_channel_indices(device_channel_indices) + if set(probe_channel_names) == set(self.channel_ids): + device_channel_indices = [] + probe_channel_names = list(probe_channel_names) + device_channel_indices = np.zeros(len(self.channel_ids), dtype=int) + for i, ch in enumerate(self.channel_ids): + index_in_probe = probe_channel_names.index(ch) + device_channel_indices[index_in_probe] = i + probe.set_device_channel_indices(device_channel_indices) + else: + warnings.warn( + "Channel names in the probe do not match the channel ids from Neo. " + "Cannot set device channel indices, but this might lead to incorrect probe geometries" + ) if probe.shank_ids is not None: self.set_probe(probe, in_place=True, group_mode="by_shank") diff --git a/src/spikeinterface/extractors/tests/test_neoextractors.py b/src/spikeinterface/extractors/tests/test_neoextractors.py index f80f62ebf0..f40b4d05ab 100644 --- a/src/spikeinterface/extractors/tests/test_neoextractors.py +++ b/src/spikeinterface/extractors/tests/test_neoextractors.py @@ -121,6 +121,9 @@ class OpenEphysBinaryRecordingTest(RecordingCommonTestSuite, unittest.TestCase): ("openephysbinary/v0.5.x_two_nodes", {"stream_id": "0"}), ("openephysbinary/v0.5.x_two_nodes", {"stream_id": "1"}), ("openephysbinary/v0.6.x_neuropixels_multiexp_multistream", {"stream_id": "0", "block_index": 0}), + # TODO: block_indices 1/2 of v0.6.x_neuropixels_multiexp_multistream have a mismatch in the channel names between + # the settings files (starting with CH0) and structure.oebin (starting at CH1). + # Currently, the extractor will skip remapping to match order in oebin and settings file, raising a warning ("openephysbinary/v0.6.x_neuropixels_multiexp_multistream", {"stream_id": "1", "block_index": 1}), ( "openephysbinary/v0.6.x_neuropixels_multiexp_multistream", From 62f79b47322120a25980dfe4504d968dae0da951 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Tue, 17 Mar 2026 18:00:22 +0100 Subject: [PATCH 3/6] Propagate oebin_file to probeinterface.read_openephys --- .../extractors/neoextractors/openephys.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/spikeinterface/extractors/neoextractors/openephys.py b/src/spikeinterface/extractors/neoextractors/openephys.py index c6774e622e..955d8a6b2d 100644 --- a/src/spikeinterface/extractors/neoextractors/openephys.py +++ b/src/spikeinterface/extractors/neoextractors/openephys.py @@ -319,22 +319,27 @@ def __init__( else: record_node = "" oe_stream = stream_name - exp_ids = sorted(list(self.neo_reader.folder_structure[record_node]["experiments"].keys())) + node_structure = self.neo_reader.folder_structure[record_node] + exp_ids = sorted(list(node_structure["experiments"].keys())) if block_index is None: exp_id = exp_ids[0] else: exp_id = exp_ids[block_index] - rec_ids = sorted( - list(self.neo_reader.folder_structure[record_node]["experiments"][exp_id]["recordings"].keys()) - ) + rec_ids = sorted(list(node_structure["experiments"][exp_id]["recordings"].keys())) # do not load probe for NIDQ stream or if load_sync_channel is True if "NI-DAQmx" not in stream_name and not load_sync_channel: - settings_file = self.neo_reader.folder_structure[record_node]["experiments"][exp_id]["settings_file"] + settings_file = node_structure["experiments"][exp_id]["settings_file"] if Path(settings_file).is_file(): + # look for oebin file + exp_name = node_structure["experiments"][exp_id]["name"] + rec_name = node_structure["experiments"][exp_id]["recordings"][rec_ids[0]]["name"] + oebin_file = settings_file.parent / exp_name / rec_name / "structure.oebin" + oebin_file = oebin_file if oebin_file.is_file() else None + probe = probeinterface.read_openephys( - settings_file=settings_file, stream_name=stream_name, raise_error=False + settings_file=settings_file, stream_name=stream_name, oebin_file=oebin_file, raise_error=False ) else: probe = None From 305b974831dab290d482d24d55c1e8e094ed45a6 Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Tue, 17 Mar 2026 18:03:48 +0100 Subject: [PATCH 4/6] Remove old logic and add comment --- .../extractors/neoextractors/openephys.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/spikeinterface/extractors/neoextractors/openephys.py b/src/spikeinterface/extractors/neoextractors/openephys.py index 955d8a6b2d..9eb0c74e24 100644 --- a/src/spikeinterface/extractors/neoextractors/openephys.py +++ b/src/spikeinterface/extractors/neoextractors/openephys.py @@ -334,6 +334,8 @@ def __init__( if Path(settings_file).is_file(): # look for oebin file exp_name = node_structure["experiments"][exp_id]["name"] + # we can use the first recording folder to find the oebin file, as the mapping should be the same + # for all recordings within the experiment rec_name = node_structure["experiments"][exp_id]["recordings"][rec_ids[0]]["name"] oebin_file = settings_file.parent / exp_name / rec_name / "structure.oebin" oebin_file = oebin_file if oebin_file.is_file() else None @@ -345,23 +347,6 @@ def __init__( probe = None if probe is not None: - # Ensure device channel index corresponds to channel_ids - probe_channel_names = probe.contact_annotations.get("channel_name", None) - if probe_channel_names is not None and not np.array_equal(probe_channel_names, self.channel_ids): - if set(probe_channel_names) == set(self.channel_ids): - device_channel_indices = [] - probe_channel_names = list(probe_channel_names) - device_channel_indices = np.zeros(len(self.channel_ids), dtype=int) - for i, ch in enumerate(self.channel_ids): - index_in_probe = probe_channel_names.index(ch) - device_channel_indices[index_in_probe] = i - probe.set_device_channel_indices(device_channel_indices) - else: - warnings.warn( - "Channel names in the probe do not match the channel ids from Neo. " - "Cannot set device channel indices, but this might lead to incorrect probe geometries" - ) - if probe.shank_ids is not None: self.set_probe(probe, in_place=True, group_mode="by_shank") else: From 4dd501e9a23798e22a3295e2cc1aa2d920c1ea7e Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Wed, 18 Mar 2026 12:47:28 +0100 Subject: [PATCH 5/6] Add tests with onebox and non-trivial wiring --- .../extractors/neoextractors/openephys.py | 18 ++++--------- .../extractors/neoextractors/spikeglx.py | 2 +- .../extractors/neuropixels_utils.py | 5 +--- .../extractors/tests/test_neoextractors.py | 25 ++++++++++++++++--- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/spikeinterface/extractors/neoextractors/openephys.py b/src/spikeinterface/extractors/neoextractors/openephys.py index 9eb0c74e24..b019280c9a 100644 --- a/src/spikeinterface/extractors/neoextractors/openephys.py +++ b/src/spikeinterface/extractors/neoextractors/openephys.py @@ -315,10 +315,10 @@ def __init__( # find settings file if "#" in stream_name: - record_node, oe_stream = stream_name.split("#") + record_node, oe_stream_name = stream_name.split("#") else: record_node = "" - oe_stream = stream_name + oe_stream_name = stream_name node_structure = self.neo_reader.folder_structure[record_node] exp_ids = sorted(list(node_structure["experiments"].keys())) if block_index is None: @@ -332,16 +332,8 @@ def __init__( settings_file = node_structure["experiments"][exp_id]["settings_file"] if Path(settings_file).is_file(): - # look for oebin file - exp_name = node_structure["experiments"][exp_id]["name"] - # we can use the first recording folder to find the oebin file, as the mapping should be the same - # for all recordings within the experiment - rec_name = node_structure["experiments"][exp_id]["recordings"][rec_ids[0]]["name"] - oebin_file = settings_file.parent / exp_name / rec_name / "structure.oebin" - oebin_file = oebin_file if oebin_file.is_file() else None - probe = probeinterface.read_openephys( - settings_file=settings_file, stream_name=stream_name, oebin_file=oebin_file, raise_error=False + settings_file=settings_file, stream_name=oe_stream_name, raise_error=False ) else: probe = None @@ -352,7 +344,7 @@ def __init__( else: self.set_probe(probe, in_place=True) # get inter-sample shifts based on the probe information and mux channels - sample_shifts = get_neuropixels_sample_shifts_from_probe(probe, stream_name=self.stream_name) + sample_shifts = get_neuropixels_sample_shifts_from_probe(probe) if sample_shifts is not None: self.set_property("inter_sample_shift", sample_shifts) @@ -361,7 +353,7 @@ def __init__( stream_folders = [] for segment_index, rec_id in enumerate(rec_ids): stream_folder = ( - recording_folder / f"experiment{exp_id}" / f"recording{rec_id}" / "continuous" / oe_stream + recording_folder / f"experiment{exp_id}" / f"recording{rec_id}" / "continuous" / oe_stream_name ) stream_folders.append(stream_folder) if load_sync_timestamps: diff --git a/src/spikeinterface/extractors/neoextractors/spikeglx.py b/src/spikeinterface/extractors/neoextractors/spikeglx.py index 79a7eb96e6..56e2dbb3fd 100644 --- a/src/spikeinterface/extractors/neoextractors/spikeglx.py +++ b/src/spikeinterface/extractors/neoextractors/spikeglx.py @@ -94,7 +94,7 @@ def __init__( self.set_probe(probe, in_place=True) # get inter-sample shifts based on the probe information and mux channels - sample_shifts = get_neuropixels_sample_shifts_from_probe(probe, stream_name=self.stream_name) + sample_shifts = get_neuropixels_sample_shifts_from_probe(probe) if sample_shifts is not None: self.set_property("inter_sample_shift", sample_shifts) else: diff --git a/src/spikeinterface/extractors/neuropixels_utils.py b/src/spikeinterface/extractors/neuropixels_utils.py index 9ecb71b414..39388e77ff 100644 --- a/src/spikeinterface/extractors/neuropixels_utils.py +++ b/src/spikeinterface/extractors/neuropixels_utils.py @@ -4,7 +4,7 @@ from probeinterface import Probe -def get_neuropixels_sample_shifts_from_probe(probe: Probe, stream_name: str = "ap") -> np.ndarray: +def get_neuropixels_sample_shifts_from_probe(probe: Probe) -> np.ndarray: """ Get the inter-sample shifts for Neuropixels probes based on the probe information. @@ -12,9 +12,6 @@ def get_neuropixels_sample_shifts_from_probe(probe: Probe, stream_name: str = "a ---------- probe : Probe The probe object containing channel and ADC information. - stream_name : str, default: "ap" - The name of the stream for which to calculate the sample shifts. - This is used for Neuropixels 1.0 technology to correctly set the number of cycles. Returns ------- diff --git a/src/spikeinterface/extractors/tests/test_neoextractors.py b/src/spikeinterface/extractors/tests/test_neoextractors.py index f40b4d05ab..9b390fbeb6 100644 --- a/src/spikeinterface/extractors/tests/test_neoextractors.py +++ b/src/spikeinterface/extractors/tests/test_neoextractors.py @@ -121,9 +121,6 @@ class OpenEphysBinaryRecordingTest(RecordingCommonTestSuite, unittest.TestCase): ("openephysbinary/v0.5.x_two_nodes", {"stream_id": "0"}), ("openephysbinary/v0.5.x_two_nodes", {"stream_id": "1"}), ("openephysbinary/v0.6.x_neuropixels_multiexp_multistream", {"stream_id": "0", "block_index": 0}), - # TODO: block_indices 1/2 of v0.6.x_neuropixels_multiexp_multistream have a mismatch in the channel names between - # the settings files (starting with CH0) and structure.oebin (starting at CH1). - # Currently, the extractor will skip remapping to match order in oebin and settings file, raising a warning ("openephysbinary/v0.6.x_neuropixels_multiexp_multistream", {"stream_id": "1", "block_index": 1}), ( "openephysbinary/v0.6.x_neuropixels_multiexp_multistream", @@ -134,8 +131,30 @@ class OpenEphysBinaryRecordingTest(RecordingCommonTestSuite, unittest.TestCase): "openephysbinary/v0.6.x_neuropixels_multiexp_multistream", {"stream_id": "2", "block_index": 2, "load_sync_timestamps": True}, ), + ( + "openephysbinary/v0.6.x_onebox_neuropixels", + {"stream_name": "Record Node 101#OneBox-100.ProbeA-AP", "block_index": 0}, + ), + ( + "openephysbinary/v0.6.x_onebox_neuropixels_nontrivial_wiring", + {"stream_name": "Record Node 101#OneBox-111.ProbeA", "block_index": 0}, + ), ] + def test_non_trivial_wiring(self): + """ + Test that we can load the probe information and sample shifts for a one box neuropixels recording with + non trivial wiring. + """ + folder_path = local_folder / "openephysbinary/v0.6.x_onebox_neuropixels_nontrivial_wiring" + stream_name = "Record Node 101#OneBox-111.ProbeA" + block_index = 0 + + recording = self.ExtractorClass(folder_path, stream_name=stream_name, block_index=block_index) + # check that channel_ids and settings_channel_key contact annotations are correctly loaded + probe = recording.get_probe() + np.testing.assert_array_equal(recording.channel_ids, probe.contact_annotations["settings_channel_key"]) + class OpenEphysBinaryEventTest(EventCommonTestSuite, unittest.TestCase): ExtractorClass = OpenEphysBinaryEventExtractor From 24705b999418e6739bd1450d75b1676ac39b1dda Mon Sep 17 00:00:00 2001 From: Alessio Buccino Date: Wed, 18 Mar 2026 13:18:35 +0100 Subject: [PATCH 6/6] import numpy --- src/spikeinterface/extractors/tests/test_neoextractors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/spikeinterface/extractors/tests/test_neoextractors.py b/src/spikeinterface/extractors/tests/test_neoextractors.py index 9b390fbeb6..b90a6da53b 100644 --- a/src/spikeinterface/extractors/tests/test_neoextractors.py +++ b/src/spikeinterface/extractors/tests/test_neoextractors.py @@ -6,6 +6,7 @@ import importlib.util import pytest +import numpy as np from spikeinterface import get_global_dataset_folder from spikeinterface.extractors.extractor_classes import (