2121from . import __version__
2222from .probe import Probe
2323from .probegroup import ProbeGroup
24+ from .neuropixels_tools import build_neuropixels_probe
2425from .utils import import_safely
2526
2627
@@ -749,15 +750,6 @@ def read_spikegadgets(file: str | Path, raise_error: bool = True) -> ProbeGroup:
749750 probe_group : ProbeGroup object
750751
751752 """
752- # ------------------------- #
753- # Npix 1.0 constants #
754- # ------------------------- #
755- TOTAL_NPIX_ELECTRODES = 960
756- MAX_ACTIVE_CHANNELS = 384
757- CONTACT_WIDTH = 16 # um
758- CONTACT_HEIGHT = 20 # um
759- # ------------------------- #
760-
761753 # Read the header and get Configuration elements
762754 header_txt = parse_spikegadgets_header (file )
763755 root = ElementTree .fromstring (header_txt )
@@ -773,122 +765,62 @@ def read_spikegadgets(file: str | Path, raise_error: bool = True) -> ProbeGroup:
773765 raise Exception ("No Neuropixels 1.0 probes found" )
774766 return None
775767
776- # Container to store Probe objects
777768 probe_group = ProbeGroup ()
778769
779770 for curr_probe in range (1 , n_probes + 1 ):
780771 probe_config = probe_configs [curr_probe - 1 ]
781772
782- # Get number of active channels from probe Device element
773+ # Get active electrode indices from the channelsOn bitmask (960 elements, 0-indexed)
783774 active_channel_str = [option for option in probe_config if option .attrib ["name" ] == "channelsOn" ][0 ].attrib [
784775 "data"
785776 ]
786- active_channels = [int (ch ) for ch in active_channel_str .split (" " ) if ch ]
787- n_active_channels = sum (active_channels )
788- assert len (active_channels ) == TOTAL_NPIX_ELECTRODES
789- assert n_active_channels <= MAX_ACTIVE_CHANNELS
790-
791- """
792- Within the SpikeConfiguration header element (sconf), there is a SpikeNTrode element
793- for each electrophysiology channel that contains information relevant to scaling and
794- otherwise displaying the information from that channel, as well as the id of the electrode
795- from which it is recording ('id').
796-
797- Nested within each SpikeNTrode element is a SpikeChannel element with information about
798- the electrode dynamically connected to that channel. This contains information relevant
799- for spike sorting, i.e., its spatial location along the probe shank and the hardware channel
800- to which it is connected.
801-
802- Excerpt of a sample SpikeConfiguration element:
803-
804- <SpikeConfiguration chanPerChip="1889715760" device="neuropixels1" categories="">
805- <SpikeNTrode viewLFPBand="0"
806- viewStimBand="0"
807- id="1384" # @USE: The first digit is the probe number; the last three digits are the electrode number
808- lfpScalingToUv="0.018311105685598315"
809- LFPChan="1"
810- notchFreq="60"
811- rawRefOn="0"
812- refChan="1"
813- viewSpikeBand="1"
814- rawScalingToUv="0.018311105685598315" # For Neuropixels 1.0, raw and spike scaling are identical
815- spikeScalingToUv="0.018311105685598315" # Extracted when reading the raw data
816- refNTrodeID="1"
817- notchBW="10"
818- color="#c83200"
819- refGroup="2"
820- filterOn="1"
821- LFPHighFilter="200"
822- moduleDataOn="0"
823- groupRefOn="0"
824- lowFilter="600"
825- refOn="0"
826- notchFilterOn="0"
827- lfpRefOn="0"
828- lfpFilterOn="0"
829- highFilter="6000"
830- >
831- <SpikeChannel thresh="60"
832- coord_dv="-480" # @USE: dorsal-ventral coordinate in um (in pairs for staggered probe)
833- spikeSortingGroup="1782505664"
834- triggerOn="1"
835- stimCapable="0"
836- coord_ml="3192" # @USE: medial-lateral coordinate in um
837- coord_ap="3700" # doesn't vary, assuming the shank's flat face is along the ML axis
838- maxDisp="400"
839- hwChan="735" # @USE: unique device channel that is reading from electrode
840- />
841- </SpikeNTrode>
842- ...
843- </SpikeConfiguration>
844- """
845- # Find all channels/electrodes that belong to the current probe
846- contact_ids = []
847- device_channels = []
848- positions = np .zeros ((n_active_channels , 2 ))
849-
850- nt_i = 0 # Both probes are in sconf, so need an independent counter of probe electrodes while iterating through
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
809+ # 0-based electrode indices, so catalogue_index = electrode_number - 1.
810+ electrode_to_hwchan = {}
851811 for ntrode in sconf :
852812 electrode_id = ntrode .attrib ["id" ]
853- if int (electrode_id [0 ]) == curr_probe : # first digit of electrode id is probe number
854- contact_ids .append (electrode_id )
855- positions [nt_i , :] = (ntrode [0 ].attrib ["coord_ml" ], ntrode [0 ].attrib ["coord_dv" ])
856- device_channels .append (ntrode [0 ].attrib ["hwChan" ])
857- nt_i += 1
858- assert len (contact_ids ) == n_active_channels
859-
860- # Construct Probe object
861- probe = Probe (ndim = 2 , si_units = "um" , model_name = "Neuropixels 1.0" , manufacturer = "IMEC" )
862- probe .set_contacts (
863- contact_ids = contact_ids ,
864- positions = positions ,
865- shapes = "square" ,
866- shank_ids = None ,
867- shape_params = {"width" : CONTACT_WIDTH , "height" : CONTACT_HEIGHT },
868- )
813+ if int (electrode_id [0 ]) == curr_probe :
814+ catalogue_index = int (electrode_id [1 :]) - 1
815+ hw_chan = int (ntrode [0 ].attrib ["hwChan" ])
816+ electrode_to_hwchan [catalogue_index ] = hw_chan
869817
870- # Wire it (i.e., point contact/electrode ids to corresponding hardware/channel ids )
818+ device_channels = np . array ([ electrode_to_hwchan [ idx ] for idx in active_indices ] )
871819 probe .set_device_channel_indices (device_channels )
872820
873- # Create a nice polygon background when plotting the probes
874- x_min = positions [:, 0 ].min ()
875- x_max = positions [:, 0 ].max ()
876- x_mid = 0.5 * (x_max + x_min )
877- y_min = positions [:, 1 ].min ()
878- y_max = positions [:, 1 ].max ()
879- polygon_default = [
880- (x_min - 20 , y_min - CONTACT_HEIGHT / 2 ),
881- (x_mid , y_min - 100 ),
882- (x_max + 20 , y_min - CONTACT_HEIGHT / 2 ),
883- (x_max + 20 , y_max + 20 ),
884- (x_min - 20 , y_max + 20 ),
885- ]
886- probe .set_planar_contour (polygon_default )
887-
888- # If there are multiple probes, they must be shifted such that they don't occupy the same coordinates.
821+ # Shift multiple probes so they don't overlap when plotted
889822 probe .move ([250 * (curr_probe - 1 ), 0 ])
890823
891- # Add the probe to the probe container
892824 probe_group .add_probe (probe )
893825
894826 return probe_group
0 commit comments