Skip to content

Commit f5bdd08

Browse files
committed
Add spikegadgets 2.0 support
1 parent 38522cc commit f5bdd08

4 files changed

Lines changed: 1308 additions & 44 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ uv.lock
3030
# libraries
3131
**/neuropixels_library_generated
3232
**/cambridgeneurotech_library
33+
.codex

src/probeinterface/io.py

Lines changed: 43 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -733,10 +733,29 @@ def write_csv(file, probe):
733733
raise NotImplementedError
734734

735735

736+
_SPIKEGADGETS_NEUROPIXELS_FORMATS = {
737+
# SpikeConfiguration.device -> (HardwareConfiguration device name, hardcoded part number, multi-probe x-shift um)
738+
#
739+
# The SpikeGadgets .rec XML does not include a probe part number. For each
740+
# family (NP1 and NP2 4-shank) the listed catalogue variants share identical
741+
# 2D geometry in the probeinterface catalogue (contact positions, pitch,
742+
# stagger, shank spacing, shank width), differing only in metadata that
743+
# probeinterface does not consume (ADC resolution, databus phase, gain,
744+
# on-shank reference, shank thickness). So hardcoding one representative
745+
# part number produces correct geometry. `model_name` and `description` are
746+
# cleared on the sliced probe to avoid claiming a specific variant.
747+
#
748+
# NP1 family: NP1000, NP1001, PRB_1_2_0480_2, PRB_1_4_0480_1, PRB_1_4_0480_1_C.
749+
# NP2 4-shank family: NP2010, NP2013, NP2014, NP2020, NP2021.
750+
"neuropixels1": ("NeuroPixels1", "NP1000", 250.0),
751+
"neuropixels2": ("NeuroPixels2", "NP2014", 1000.0),
752+
}
753+
754+
736755
def read_spikegadgets(file: str | Path, raise_error: bool = True) -> ProbeGroup:
737756
"""
738757
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),
758+
SpikeGadgets headstages support up to three Neuropixels probes (1.0 or 2.0),
740759
and information for all probes will be returned in a ProbeGroup object.
741760
742761
@@ -750,63 +769,34 @@ def read_spikegadgets(file: str | Path, raise_error: bool = True) -> ProbeGroup:
750769
probe_group : ProbeGroup object
751770
752771
"""
753-
# Read the header and get Configuration elements
754772
header_txt = parse_spikegadgets_header(file)
755773
root = ElementTree.fromstring(header_txt)
756774
hconf = root.find("HardwareConfiguration")
757775
sconf = root.find("SpikeConfiguration")
758776

759-
# Get number of probes present (each has its own Device element)
760-
probe_configs = [device for device in hconf if device.attrib["name"] == "NeuroPixels1"]
777+
# SpikeConfiguration.device selects the Neuropixels family. Default to NP1
778+
# when absent to preserve behavior for older files that predate the attribute.
779+
sconf_device = (sconf.attrib.get("device", "") if sconf is not None else "").lower()
780+
if sconf_device not in _SPIKEGADGETS_NEUROPIXELS_FORMATS:
781+
sconf_device = "neuropixels1"
782+
hc_device_name, part_number, multi_probe_x_shift_um = _SPIKEGADGETS_NEUROPIXELS_FORMATS[sconf_device]
783+
784+
probe_configs = [d for d in hconf if d.attrib.get("name") == hc_device_name]
761785
n_probes = len(probe_configs)
762786

763787
if n_probes == 0:
764788
if raise_error:
765-
raise Exception("No Neuropixels 1.0 probes found")
789+
raise Exception(f"No {hc_device_name} devices found in SpikeGadgets .rec header")
766790
return None
767791

768792
probe_group = ProbeGroup()
769793

770794
for curr_probe in range(1, n_probes + 1):
771-
probe_config = probe_configs[curr_probe - 1]
772-
773-
# Get active electrode indices from the channelsOn bitmask (960 elements, 0-indexed)
774-
active_channel_str = [option for option in probe_config if option.attrib["name"] == "channelsOn"][0].attrib[
775-
"data"
776-
]
777-
channels_on = np.array([int(ch) for ch in active_channel_str.split(" ") if ch])
778-
active_indices = np.nonzero(channels_on)[0]
779-
780-
# Build full catalogue probe and slice to active electrodes.
781-
#
782-
# The SpikeGadgets XML format does not include the probe part number, so we
783-
# hardcode "NP1000" (standard Neuropixels 1.0). This is safe because the
784-
# Bennu manual (Rev3, 2025) explicitly states support for "Neuropixels 1.0
785-
# probes (every version except NHP) OR Neuropixels 2.0". The supported
786-
# NP 1.0 variants (NP1000, NP1001, PRB_1_2_0480_2, PRB_1_4_0480_1,
787-
# PRB_1_4_0480_1_C) share identical 2D geometry in the catalogue: same
788-
# contact positions, pitch, stagger, shank width, tip length, shank length,
789-
# contour, electrode count, and ADC/MUX tables. The only fields that differ
790-
# are metadata (description, datasheet, is_commercial) and shank_thickness_um
791-
# (Z-axis), none of which probeinterface uses.
792-
#
793-
# NP 2.0 support: the Bennu uses a different cone and firmware for 2.0
794-
# probes, and the workspace creation step distinguishes "Neuropixels 1.0"
795-
# from "Neuropixels 2.0". When .rec files from NP 2.0 recordings become
796-
# available, this reader will need to detect the probe type (likely from
797-
# the device name in the XML) and call build_neuropixels_probe with the
798-
# appropriate part number.
799-
full_probe = build_neuropixels_probe("NP1000")
800-
probe = full_probe.get_slice(active_indices)
801-
802-
# Clear part-number-specific metadata since we don't know the actual part number.
803-
probe.model_name = ""
804-
probe.description = ""
805-
806-
# Parse SpikeNTrode elements to build the device channel mapping.
807-
# Each SpikeNTrode has an id like "1384" where the first digit is the probe number
808-
# and the remaining digits are the 1-based electrode number. The catalogue uses
795+
# SpikeNTrode elements are the authoritative list of recorded electrodes.
796+
# Each id is "<probe_digit><1-based electrode number>"; the catalogue uses
809797
# 0-based electrode indices, so catalogue_index = electrode_number - 1.
798+
# This holds for both NP1 (up to 960 electrodes) and NP2 4-shank (up to
799+
# 5120 electrodes, shank-major in the catalogue: s0e0..s0e1279, s1e0..).
810800
electrode_to_hwchan = {}
811801
for ntrode in sconf:
812802
electrode_id = ntrode.attrib["id"]
@@ -815,11 +805,20 @@ def read_spikegadgets(file: str | Path, raise_error: bool = True) -> ProbeGroup:
815805
hw_chan = int(ntrode[0].attrib["hwChan"])
816806
electrode_to_hwchan[catalogue_index] = hw_chan
817807

808+
active_indices = np.array(sorted(electrode_to_hwchan.keys()))
809+
810+
full_probe = build_neuropixels_probe(part_number)
811+
probe = full_probe.get_slice(active_indices)
812+
813+
# Clear part-number-specific metadata since we don't know the actual part number.
814+
probe.model_name = ""
815+
probe.description = ""
816+
818817
device_channels = np.array([electrode_to_hwchan[idx] for idx in active_indices])
819818
probe.set_device_channel_indices(device_channels)
820819

821820
# Shift multiple probes so they don't overlap when plotted
822-
probe.move([250 * (curr_probe - 1), 0])
821+
probe.move([multi_probe_x_shift_um * (curr_probe - 1), 0])
823822

824823
probe_group.add_probe(probe)
825824

0 commit comments

Comments
 (0)