Skip to content

Commit 3e8a238

Browse files
authored
Add Neuropixels 2.0 support to read_spikegadgets_neuropixels (#441)
1 parent 4e410ec commit 3e8a238

5 files changed

Lines changed: 1682 additions & 195 deletions

File tree

src/probeinterface/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
write_csv,
1515
read_BIDS_probe,
1616
write_BIDS_probe,
17-
read_spikegadgets,
18-
read_spikegadgets_neuropixels,
19-
has_spikegadgets_neuropixels_probes,
2017
read_mearec,
2118
read_nwb,
2219
read_maxwell,
@@ -33,6 +30,10 @@
3330
read_openephys_neuropixels,
3431
has_neuropixels_probes,
3532
get_saved_channel_indices_from_openephys_settings,
33+
read_spikegadgets,
34+
read_spikegadgets_neuropixels,
35+
has_spikegadgets_neuropixels_probes,
36+
parse_spikegadgets_header,
3637
)
3738
from .utils import combine_probes
3839
from .generator import (

src/probeinterface/io.py

Lines changed: 0 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,13 @@
1111

1212
from pathlib import Path
1313
import re
14-
import warnings
1514
import json
1615
from collections import OrderedDict
1716
from packaging.version import parse
1817
import numpy as np
19-
from xml.etree import ElementTree
20-
2118
from . import __version__
2219
from .probe import Probe
2320
from .probegroup import ProbeGroup
24-
from .neuropixels_tools import build_neuropixels_probe, _annotate_probe_with_adc_sampling_info
2521
from .utils import import_safely
2622

2723

@@ -733,193 +729,6 @@ def write_csv(file, probe):
733729
raise NotImplementedError
734730

735731

736-
def read_spikegadgets_neuropixels(file: str | Path, raise_error: bool = True) -> ProbeGroup:
737-
"""
738-
Find active channels of the given Neuropixels probe from a SpikeGadgets .rec file.
739-
SpikeGadgets headstages support up to three Neuropixels 1.0 probes (as of March 28, 2024),
740-
and information for all probes will be returned in a ProbeGroup object.
741-
742-
This function only supports Neuropixels probes recorded with SpikeGadgets
743-
headstages (``HardwareConfiguration`` entries with ``name == "NeuroPixels1"``).
744-
It does not handle tetrodes or other probe types that SpikeGadgets can
745-
record. Use :func:`has_spikegadgets_neuropixels_probes` to check whether a
746-
``.rec`` file contains Neuropixels probe geometry before calling this reader.
747-
748-
Parameters
749-
----------
750-
file : Path or str
751-
The .rec file path
752-
753-
Returns
754-
-------
755-
probe_group : ProbeGroup object
756-
757-
"""
758-
# The SpikeGadgets .rec XML does not include a probe part number. The NP1.0
759-
# catalogue variants (NP1000, NP1001, PRB_1_2_0480_2, PRB_1_4_0480_1,
760-
# PRB_1_4_0480_1_C) share identical 2D geometry in the probeinterface
761-
# catalogue (contact positions, pitch, stagger, shank width), differing only
762-
# in metadata that probeinterface does not consume (ADC resolution, databus
763-
# phase, gain, on-shank reference, shank thickness). So hardcoding NP1000
764-
# produces correct geometry; `model_name` and `description` are cleared on
765-
# the sliced probe to avoid claiming a specific variant.
766-
PART_NUMBER = "NP1000"
767-
768-
header_txt = parse_spikegadgets_header(file)
769-
root = ElementTree.fromstring(header_txt)
770-
hconf = root.find("HardwareConfiguration")
771-
sconf = root.find("SpikeConfiguration")
772-
773-
probe_configs = [d for d in hconf if d.attrib.get("name") == "NeuroPixels1"]
774-
n_probes = len(probe_configs)
775-
776-
if n_probes == 0:
777-
if raise_error:
778-
raise Exception("No Neuropixels 1.0 probes found")
779-
return None
780-
781-
# NeuroPixels1 SourceOptions blocks carry the per-probe AP/LF gain settings.
782-
# They appear in the same order as the SpikeNTrode probe digits (1, 2, 3).
783-
source_options_blocks = [s for s in hconf.findall("SourceOptions") if s.attrib.get("name") == "NeuroPixels1"]
784-
785-
probe_group = ProbeGroup()
786-
787-
for curr_probe in range(1, n_probes + 1):
788-
# SpikeNTrode elements are the authoritative list of recorded electrodes.
789-
# Each id is "<probe_digit><1-based electrode number>" for up to 960
790-
# electrodes on NP1.0; the catalogue uses 0-based indices, so
791-
# catalogue_index = electrode_number - 1. The probe number is assumed
792-
# to be a single digit (1, 2, or 3), matching the documented
793-
# SpikeGadgets limit of three simultaneous Neuropixels probes.
794-
electrode_to_hwchan = {}
795-
for ntrode in sconf:
796-
electrode_id = ntrode.attrib["id"]
797-
if int(electrode_id[0]) == curr_probe:
798-
catalogue_index = int(electrode_id[1:]) - 1
799-
hw_chan = int(ntrode[0].attrib["hwChan"])
800-
electrode_to_hwchan[catalogue_index] = hw_chan
801-
802-
active_indices = np.array(sorted(electrode_to_hwchan.keys()))
803-
804-
full_probe = build_neuropixels_probe(PART_NUMBER)
805-
probe = full_probe.get_slice(active_indices)
806-
807-
# Clear part-number-specific metadata since we don't know the actual part number.
808-
probe.model_name = ""
809-
probe.description = ""
810-
811-
device_channels = np.array([electrode_to_hwchan[idx] for idx in active_indices])
812-
probe.set_device_channel_indices(device_channels)
813-
814-
# Per-contact ADC group and sample order from the catalogue MUX table plus
815-
# the hwChan mapping (which is the readout-channel index for each contact).
816-
adc_sampling_table = probe.annotations.get("adc_sampling_table")
817-
_annotate_probe_with_adc_sampling_info(probe, adc_sampling_table)
818-
819-
# NP1.0 gain is programmable. Read APGainMode and LFPGainMode from the
820-
# SourceOptions block matching this probe (blocks appear in probe order).
821-
if "ap_gain" not in probe.annotations and curr_probe - 1 < len(source_options_blocks):
822-
custom_options = {
823-
opt.attrib["name"]: opt.attrib["data"].strip()
824-
for opt in source_options_blocks[curr_probe - 1].findall("CustomOption")
825-
}
826-
ap_gain_str = custom_options.get("APGainMode")
827-
if ap_gain_str:
828-
probe.annotate(ap_gain=float(ap_gain_str))
829-
if probe.annotations.get("lf_sample_frequency_hz", 0) > 0:
830-
lf_gain_str = custom_options.get("LFPGainMode")
831-
if lf_gain_str:
832-
probe.annotate(lf_gain=float(lf_gain_str))
833-
834-
# Shift multiple probes so they don't overlap when plotted
835-
probe.move([250 * (curr_probe - 1), 0])
836-
837-
probe_group.add_probe(probe)
838-
839-
return probe_group
840-
841-
842-
def read_spikegadgets(*args, **kwargs) -> ProbeGroup:
843-
"""
844-
Deprecated alias for :func:`read_spikegadgets_neuropixels`.
845-
846-
The name ``read_spikegadgets`` is misleading because the function only reads
847-
Neuropixels probe geometry, not arbitrary SpikeGadgets ``.rec`` recordings.
848-
Use :func:`read_spikegadgets_neuropixels` instead, and
849-
:func:`has_spikegadgets_neuropixels_probes` to check whether a ``.rec`` file
850-
has Neuropixels geometry before calling it.
851-
"""
852-
warnings.warn(
853-
"read_spikegadgets is deprecated and will be removed in a future release. "
854-
"Use read_spikegadgets_neuropixels instead.",
855-
category=DeprecationWarning,
856-
stacklevel=2,
857-
)
858-
return read_spikegadgets_neuropixels(*args, **kwargs)
859-
860-
861-
def has_spikegadgets_neuropixels_probes(file: str | Path) -> bool:
862-
"""
863-
Return True if the SpikeGadgets ``.rec`` file describes at least one
864-
Neuropixels probe.
865-
866-
Detection scans the ``HardwareConfiguration`` block of the ``.rec`` XML
867-
header for ``Device`` entries whose ``name`` attribute matches a known
868-
Neuropixels source name (currently ``"NeuroPixels1"``). The presence of
869-
any such entry is the ground-truth signal that the file contains
870-
Neuropixels probe geometry, independent of what other hardware the
871-
headstage is also streaming.
872-
873-
Intended use: callers that route heterogeneous SpikeGadgets recordings
874-
(mixing tetrodes, Neuropixels, etc.) can gate the call to
875-
:func:`read_spikegadgets_neuropixels` on this helper and skip probe
876-
attachment for non-Neuropixels recordings.
877-
878-
Parameters
879-
----------
880-
file : str or Path
881-
Path to the SpikeGadgets ``.rec`` file.
882-
883-
Returns
884-
-------
885-
bool
886-
"""
887-
try:
888-
header_txt = parse_spikegadgets_header(file)
889-
root = ElementTree.fromstring(header_txt)
890-
except Exception:
891-
return False
892-
893-
hconf = root.find("HardwareConfiguration")
894-
if hconf is None:
895-
return False
896-
897-
for device in hconf:
898-
if device.attrib.get("name") == "NeuroPixels1":
899-
return True
900-
return False
901-
902-
903-
def parse_spikegadgets_header(file: str | Path) -> str:
904-
"""
905-
Parse file (SpikeGadgets .rec format) into a string until "</Configuration>",
906-
which is the last tag of the header, after which the binary data begins.
907-
"""
908-
header_size = None
909-
with open(file, mode="rb") as f:
910-
while True:
911-
line = f.readline()
912-
if b"</Configuration>" in line:
913-
header_size = f.tell()
914-
break
915-
916-
if header_size is None:
917-
ValueError("SpikeGadgets: the xml header does not contain '</Configuration>'")
918-
919-
f.seek(0)
920-
return f.read(header_size).decode("utf8")
921-
922-
923732
def read_mearec(file: str | Path) -> Probe:
924733
"""
925734
Read probe position, and contact shape from a MEArec file.

0 commit comments

Comments
 (0)