@@ -416,10 +416,8 @@ def build_neuropixels_probe(probe_part_number: str) -> Probe:
416416 positions = np .stack ((x_pos , y_pos ), axis = 1 )
417417
418418 # ===== 4. Calculate contact IDs =====
419- if shank_ids is not None :
420- contact_ids = [f"s{ shank_id } e{ elec_id } " for shank_id , elec_id in zip (shank_ids , elec_ids )]
421- else :
422- contact_ids = [f"e{ elec_id } " for elec_id in elec_ids ]
419+ shank_ids_iter = shank_ids if shank_ids is not None else [None ] * len (elec_ids )
420+ contact_ids = [_build_canonical_contact_id (elec_id , shank_id ) for shank_id , elec_id in zip (shank_ids_iter , elec_ids )]
423421
424422 # ===== 5. Create Probe object and set contacts =====
425423 probe = Probe (ndim = 2 , si_units = "um" , model_name = probe_part_number , manufacturer = "imec" )
@@ -693,6 +691,39 @@ def write_imro(file: str | Path, probe: Probe):
693691##
694692
695693
694+ def _build_canonical_contact_id (electrode_id : int , shank_id : int | None = None ) -> str :
695+ """
696+ Build the canonical contact ID string for a Neuropixels electrode.
697+
698+ This establishes the standard naming convention used throughout probeinterface
699+ for Neuropixels contact identification.
700+
701+ Parameters
702+ ----------
703+ electrode_id : int
704+ Physical electrode ID on the probe (e.g., 0-959 for NP1.0)
705+ shank_id : int or None, default: None
706+ Shank ID for multi-shank probes. If None, assumes single-shank probe.
707+
708+ Returns
709+ -------
710+ contact_id : str
711+ Canonical contact ID string, either "e{electrode_id}" for single-shank
712+ or "s{shank_id}e{electrode_id}" for multi-shank probes.
713+
714+ Examples
715+ --------
716+ >>> _build_canonical_contact_id(123)
717+ 'e123'
718+ >>> _build_canonical_contact_id(123, shank_id=0)
719+ 's0e123'
720+ """
721+ if shank_id is not None :
722+ return f"s{ shank_id } e{ electrode_id } "
723+ else :
724+ return f"e{ electrode_id } "
725+
726+
696727def _parse_imro_string (imro_table_string : str , probe_part_number : str ) -> dict :
697728 """
698729 Parse IMRO (Imec ReadOut) table string into structured per-channel data.
@@ -717,7 +748,10 @@ def _parse_imro_string(imro_table_string: str, probe_part_number: str) -> dict:
717748 imro_per_channel : dict
718749 Dictionary where each key maps to a list of values (one per channel).
719750 Keys are IMRO fields like "channel", "bank", "electrode", "ap_gain", etc.
720- Example: {"channel": [0,1,2,...], "bank": [1,0,0,...], "ap_gain": [500,500,...]}
751+ The "electrode" key always contains physical electrode IDs (0-959 for NP1.0, etc.).
752+ For NP2.0+: electrode IDs come directly from IMRO data.
753+ For NP1.0: electrode IDs are computed as bank * 384 + channel.
754+ Example: {"channel": [0,1,2,...], "bank": [1,0,0,...], "electrode": [384,1,2,...], "ap_gain": [500,500,...]}
721755 """
722756 # Get IMRO field format from catalogue
723757 probe_features = _load_np_probe_features ()
@@ -735,6 +769,15 @@ def _parse_imro_string(imro_table_string: str, probe_part_number: str) -> dict:
735769 for field , field_value in zip (imro_fields , values ):
736770 imro_per_channel [field ].append (field_value )
737771
772+ # Ensure "electrode" key always exists with physical electrode IDs
773+ # Different probe types encode electrode selection differently
774+ if "electrode" not in imro_per_channel :
775+ # NP1.0: Bank-based addressing (physical_electrode_id = bank * 384 + channel)
776+ readout_channel_ids = np .array (imro_per_channel ["channel" ])
777+ bank_key = "bank" if "bank" in imro_per_channel else "bank_mask"
778+ bank_indices = np .array (imro_per_channel [bank_key ])
779+ imro_per_channel ["electrode" ] = (bank_indices * 384 + readout_channel_ids ).tolist ()
780+
738781 return imro_per_channel
739782
740783
@@ -790,41 +833,32 @@ def read_spikeglx(file: str | Path) -> Probe:
790833 imro_table_string = meta ["imroTbl" ]
791834 imro_per_channel = _parse_imro_string (imro_table_string , imDatPrb_pn )
792835
793- # ===== 4. Get active electrodes from IMRO data =====
794- # Convert IMRO channel numbers to physical electrode IDs. Different probe types encode
795- # electrode selection differently: NP1.0 uses banks, NP2.0+ uses direct electrode IDs.
796- if "electrode" in imro_per_channel :
797- # NP2.0+: Direct electrode addressing
798- physical_electrode_ids = np .array (imro_per_channel ["electrode" ])
799- else :
800- # NP1.0: Bank-based addressing (physical_electrode_id = bank * 384 + channel)
801- readout_channel_ids = np .array (imro_per_channel ["channel" ])
802- bank_key = "bank" if "bank" in imro_per_channel else "bank_mask"
803- bank_indices = np .array (imro_per_channel [bank_key ])
804- physical_electrode_ids = bank_indices * 384 + readout_channel_ids
836+ # ===== 4. Build contact IDs for active electrodes =====
837+ # Convert physical electrode IDs to probeinterface canonical contact ID strings
838+ imro_electrode = imro_per_channel ["electrode" ]
839+ imro_shank = imro_per_channel .get ("shank" , [None ] * len (imro_electrode ))
840+ active_contact_ids = [
841+ _build_canonical_contact_id (elec_id , shank_id )
842+ for shank_id , elec_id in zip (imro_shank , imro_electrode )
843+ ]
805844
806845 # ===== 5. Slice full probe to active electrodes =====
807- selected_contact_indices = []
808- for idx , electrode_id in enumerate (physical_electrode_ids ):
809- if "shank" in imro_per_channel :
810- shank_id = imro_per_channel ["shank" ][idx ]
811- contact_id_str = f"s{ shank_id } e{ electrode_id } "
812- else :
813- contact_id_str = f"e{ electrode_id } "
814-
815- full_probe_index = np .where (full_probe .contact_ids == contact_id_str )[0 ]
816- if len (full_probe_index ) > 0 :
817- selected_contact_indices .append (full_probe_index [0 ])
846+ # Find indices where full probe contact IDs match the active contact IDs
847+ full_probe_contact_ids = np .array (full_probe .contact_ids )
848+ active_contact_ids_array = np .array (active_contact_ids )
849+ mask = np .isin (full_probe_contact_ids , active_contact_ids_array )
850+ selected_contact_indices = np .where (mask )[0 ]
818851
819852 probe = full_probe .get_slice (np .array (selected_contact_indices , dtype = int ))
820853
821854 # ===== 6. Store IMRO properties (acquisition settings) as annotations =====
822- vector_properties = ("channel" , "bank" , "bank_mask" , "ref_id" , "ap_gain" , "lf_gain" , "ap_hipas_flt" )
823- vector_properties_available = {}
824- for k , v in imro_per_channel .items ():
825- if (k in vector_properties ) and (len (v ) > 0 ):
826- vector_properties_available [imro_field_to_pi_field .get (k )] = v
827- probe .annotate_contacts (** vector_properties_available )
855+ # Map IMRO field names to probeinterface field names and add as contact annotations
856+ imro_properties_to_add = ("channel" , "bank" , "bank_mask" , "ref_id" , "ap_gain" , "lf_gain" , "ap_hipas_flt" )
857+ probe .annotate_contacts (** {
858+ imro_field_to_pi_field .get (k ): v
859+ for k , v in imro_per_channel .items ()
860+ if k in imro_properties_to_add and len (v ) > 0
861+ })
828862
829863 # ===== 7. Slice to saved channels (if subset was saved) =====
830864 # This is DIFFERENT from IMRO selection: IMRO selects which electrodes to acquire,
0 commit comments