@@ -353,10 +353,11 @@ def build_neuropixels_probe(probe_part_number: str) -> Probe:
353353 """
354354 Build a Neuropixels probe with all possible contacts from the probe part number.
355355
356- This function constructs a complete probe geometry including all physical contacts,
357- MUX table information, and probe-level metadata. The resulting probe contains ALL
358- electrodes (e.g., 960 for NP1.0, 1280 for NP2.0), not just the subset that might
359- be recorded in an actual experiment.
356+ This function constructs a complete probe geometry based on IMEC manufacturer specifications
357+ sourced from Bill Karsh's ProbeTable repository (https://github.com/billkarsh/ProbeTable).
358+ The specifications include contact positions, electrode dimensions, shank geometry, MUX routing
359+ tables, and ADC configurations. The resulting probe contains ALL electrodes (e.g., 960 for
360+ NP1.0, 1280 for NP2.0), not just the subset that might be recorded in an actual experiment.
360361
361362 Parameters
362363 ----------
@@ -681,65 +682,67 @@ def write_imro(file: str | Path, probe: Probe):
681682
682683def read_spikeglx (file : str | Path ) -> Probe :
683684 """
684- Read probe position for the meta file generated by SpikeGLX
685+ Read probe geometry and configuration from a SpikeGLX metadata file.
685686
686- See http://billkarsh.github.io/SpikeGLX/#metadata-guides for implementation.
687- The x_pitch/y_pitch/width are set automatically depending on the NP version.
688-
689- The shape is auto generated as a shank.
687+ This function reconstructs the probe used in a recording by:
688+ 1. Reading the probe part number from metadata
689+ 2. Building the full probe geometry from manufacturer specifications
690+ 3. Slicing to the electrodes selected in the IMRO table
691+ 4. Further slicing to channels actually saved to disk (if subset was saved)
692+ 5. Adding recording-specific annotations
693+ 6. Add wiring (device channel indices)
690694
691695 Parameters
692696 ----------
693697 file : Path or str
694- The .meta file path
698+ Path to the SpikeGLX .meta file
695699
696700 Returns
697701 -------
698- probe : Probe object
702+ probe : Probe
703+ Probe object with geometry, contact annotations, and device channel mapping
704+
705+ See Also
706+ --------
707+ http://billkarsh.github.io/SpikeGLX/#metadata-guides
699708
700709 """
701710
702711 meta_file = Path (file )
703712 assert meta_file .suffix == ".meta" , "'meta_file' should point to the .meta SpikeGLX file"
704713
705714 meta = parse_spikeglx_meta (meta_file )
706-
707715 assert "imroTbl" in meta , "Could not find imroTbl field in meta file!"
708- imro_table = meta ["imroTbl" ]
709716
710- # read serial number
711- imDatPrb_serial_number = meta .get ("imDatPrb_sn" , None )
712- if imDatPrb_serial_number is None : # this is for Phase3A
713- imDatPrb_serial_number = meta .get ("imProbeSN" , None )
714-
715- # read other metadata
717+ # ===== 1. Extract probe part number from metadata =====
716718 imDatPrb_pn = meta .get ("imDatPrb_pn" , None )
717- imDatPrb_port = meta .get ("imDatPrb_port" , None )
718- imDatPrb_slot = meta .get ("imDatPrb_slot" , None )
719- imDatPrb_part_number = meta .get ("imDatPrb_pn" , None )
720-
721- # Only Phase3a probe has "imProbeOpt". Map this to NP10101.
719+ # Only Phase3a probe has "imProbeOpt". Map this to NP1010.
722720 if meta .get ("imProbeOpt" ) is not None :
723721 imDatPrb_pn = "NP1010"
724722
725- # ===== Step 1: Build full probe with all contacts =====
726- full_probe = build_neuropixels_probe (imDatPrb_pn )
723+ # ===== 2. Build full probe with all possible contacts =====
724+ # This creates the complete probe geometry (e.g., 960 contacts for NP1.0)
725+ # based on manufacturer specifications
726+ full_probe = build_neuropixels_probe (probe_part_number = imDatPrb_pn )
727727
728- # ===== Step 2: Parse IMRO table to get active electrode IDs and settings =====
728+ # ===== 3. Parse IMRO table to extract recorded electrodes and acquisition settings =====
729+ # The IMRO table specifies which electrodes were selected for recording (e.g., 384 of 960),
730+ # plus their acquisition settings (gains, references, filters)
731+ imro_table = meta ["imroTbl" ]
729732 probe_type_num_chans , * imro_table_values_list , _ = imro_table .strip ().split (")" )
730733
731734 # probe_type_num_chans looks like f"({probe_type},{num_chans}"
732735 probe_type = probe_type_num_chans .split ("," )[0 ][1 :]
733736
734737 probe_features = _load_np_probe_features ()
735- _ , fields , _ = get_probe_metadata_from_probe_features (probe_features , imDatPrb_pn )
738+ _ , imro_fields , _ = get_probe_metadata_from_probe_features (probe_features , imDatPrb_pn )
736739
737740 # Parse IMRO table values
738- contact_info = {k : [] for k in fields }
741+ contact_info = {k : [] for k in imro_fields }
739742 for field_values_str in imro_table_values_list : # Imro table values look like '(value, value, value, ... '
740743 # Split them by space to get int('value'), int('value'), int('value'), ...)
741744 values = tuple (map (int , field_values_str [1 :].split (" " )))
742- for field , field_value in zip (fields , values ):
745+ for field , field_value in zip (imro_fields , values ):
743746 contact_info [field ].append (field_value )
744747
745748 # Extract electrode IDs from IMRO table
@@ -754,8 +757,8 @@ def read_spikeglx(file: str | Path) -> Probe:
754757 banks = np .array (contact_info [bank_key ])
755758 elec_ids = banks * 384 + channel_ids
756759
757- # ===== Step 3: Slice full probe to active electrodes =====
758- # Find indices in full probe that match the active electrode IDs from IMRO
760+ # ===== 4. Slice full probe to IMRO-selected electrodes =====
761+ # Match the electrode IDs from IMRO table to contacts in the full probe
759762 keep_indices = []
760763 for elec_id in elec_ids :
761764 # For multi-shank probes, we need to check if shank info is in contact_info
@@ -773,35 +776,38 @@ def read_spikeglx(file: str | Path) -> Probe:
773776
774777 probe = full_probe .get_slice (np .array (keep_indices , dtype = int ))
775778
776- # ===== Step 4: Add IMRO-specific annotations =====
777- # scalar annotations
779+ # Add IMRO-specific contact annotations (acquisition settings)
778780 probe .annotate (probe_type = probe_type )
779-
780- # vector annotations
781781 vector_properties = ("channel" , "bank" , "bank_mask" , "ref_id" , "ap_gain" , "lf_gain" , "ap_hipas_flt" )
782-
783782 vector_properties_available = {}
784783 for k , v in contact_info .items ():
785784 if (k in vector_properties ) and (len (v ) > 0 ):
786- # convert to ProbeInterface naming for backwards compatibility
787785 vector_properties_available [imro_field_to_pi_field .get (k )] = v
788-
789786 probe .annotate_contacts (** vector_properties_available )
790787
791- # add serial number and other annotations
788+ # ===== 5. Slice to saved channels (if subset was saved) =====
789+ # This is DIFFERENT from IMRO selection: IMRO selects which electrodes to acquire,
790+ # but SpikeGLX can optionally save only a subset of acquired channels to reduce file size.
791+ # For example: IMRO selects 384 electrodes, but only 300 are saved to disk.
792+ saved_chans = get_saved_channel_indices_from_spikeglx_meta (meta_file )
793+ saved_chans = saved_chans [saved_chans < probe .get_contact_count ()] # Remove SYS channels
794+ if saved_chans .size != probe .get_contact_count ():
795+ probe = probe .get_slice (saved_chans )
796+
797+ # ===== 6. Add recording-specific annotations =====
798+ # These annotations identify the physical probe instance and recording setup
799+ imDatPrb_serial_number = meta .get ("imDatPrb_sn" ) or meta .get ("imProbeSN" ) # Phase3A uses imProbeSN
800+ imDatPrb_port = meta .get ("imDatPrb_port" , None )
801+ imDatPrb_slot = meta .get ("imDatPrb_slot" , None )
792802 probe .annotate (serial_number = imDatPrb_serial_number )
793- probe .annotate (part_number = imDatPrb_part_number )
803+ probe .annotate (part_number = imDatPrb_pn )
794804 probe .annotate (port = imDatPrb_port )
795805 probe .annotate (slot = imDatPrb_slot )
796806
797- # sometimes we need to slice the probe when not all channels are saved
798- saved_chans = get_saved_channel_indices_from_spikeglx_meta (meta_file )
799- # remove the SYS chans
800- saved_chans = saved_chans [saved_chans < probe .get_contact_count ()]
801- if saved_chans .size != probe .get_contact_count ():
802- # slice if needed
803- probe = probe .get_slice (saved_chans )
804- # wire it
807+ # ===== 7. Set device channel indices (wiring) =====
808+ # Device channel indices map probe contacts to data file channels.
809+ # After all slicing, channels are numbered 0, 1, 2, ... N-1 in the order they appear
810+ # in the binary data file, so we wire them sequentially.
805811 probe .set_device_channel_indices (np .arange (probe .get_contact_count ()))
806812
807813 return probe
0 commit comments