@@ -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+
736755def 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