@@ -257,25 +257,21 @@ def read_imro(file_path: Union[str, Path]) -> Probe:
257257 if imDatPrb_type == probe_type :
258258 imDatPrb_pn = probe_part_number
259259
260- # ===== 2. Interpret IMRO table to extract recorded electrodes and acquisition settings =====
261- imro_per_channel = _interpret_imro_string (imro_str , imDatPrb_pn )
260+ # ===== 2. Interpret IMRO table =====
261+ imro_per_channel = _parse_imro_string (imro_str , imDatPrb_pn )
262262
263263 # ===== 3. Build full probe with all possible contacts =====
264264 full_probe = build_neuropixels_probe (probe_part_number = imDatPrb_pn )
265265
266266 # ===== 4. Build contact IDs for active electrodes =====
267- elec_ids = imro_per_channel ["electrode" ]
268- shank_ids = imro_per_channel .get ("shank" , [None ] * len (elec_ids ))
269- active_contact_ids = [
270- _build_canonical_contact_id (elec_id , shank_id ) for shank_id , elec_id in zip (shank_ids , elec_ids )
271- ]
267+ active_contact_ids = _get_active_contact_ids (imro_per_channel , imro_str )
272268
273269 # ===== 5. Slice full probe to active electrodes =====
274270 contact_id_to_index = {cid : i for i , cid in enumerate (full_probe .contact_ids )}
275271 selected_indices = np .array ([contact_id_to_index [cid ] for cid in active_contact_ids ])
276272 probe = full_probe .get_slice (selected_indices )
277273
278- # ===== 6 . Annotate probe with recording-specific metadata =====
274+ # ===== 7 . Annotate probe with recording-specific metadata =====
279275 adc_sampling_table = probe .annotations .get ("adc_sampling_table" )
280276 _annotate_probe_with_adc_sampling_info (probe , adc_sampling_table )
281277
@@ -670,7 +666,7 @@ def _build_canonical_contact_id(electrode_id: int, shank_id: int | None = None)
670666 return f"e{ electrode_id } "
671667
672668
673- def _interpret_imro_string (imro_table_string : str , probe_part_number : str ) -> dict :
669+ def _parse_imro_string (imro_table_string : str , probe_part_number : str ) -> dict :
674670 """
675671 Parse IMRO (Imec ReadOut) table string into structured per-channel data.
676672
@@ -692,12 +688,13 @@ def _interpret_imro_string(imro_table_string: str, probe_part_number: str) -> di
692688 Returns
693689 -------
694690 imro_per_channel : dict
695- Dictionary where each key maps to a list of values (one per channel).
696- Keys are IMRO fields like "channel", "bank", "electrode", "ap_gain", etc.
697- The "electrode" key always contains physical electrode IDs (0-959 for NP1.0, etc.).
698- For NP2.0+: electrode IDs come directly from IMRO data.
699- For NP1.0: electrode IDs are computed as bank * 384 + channel.
700- Example: {"channel": [0,1,2,...], "bank": [1,0,0,...], "electrode": [384,1,2,...], "ap_gain": [500,500,...]}
691+ Dictionary where each key maps to a list of values (one per entry).
692+ Keys correspond to the IMRO field names from the catalogue.
693+ NP2.x+ probes will have an "electrode" key directly. NP1.x probes will not
694+ (electrode IDs must be resolved separately via _get_active_contact_ids).
695+ Example for NP1.0: {"channel": [0,1,...], "bank": [0,0,...], "ref_id": [0,0,...], ...}
696+ Example for NP2.0: {"channel": [0,1,...], "bank_mask": [1,1,...], "electrode": [0,1,...], ...}
697+ Example for NP1110: {"group": [0,1,...], "bankA": [0,0,...], "bankB": [0,0,...]}
701698 """
702699 # Get IMRO field format from catalogue
703700 probe_features = _load_np_probe_features ()
@@ -715,15 +712,44 @@ def _interpret_imro_string(imro_table_string: str, probe_part_number: str) -> di
715712 for field , field_value in zip (imro_fields , values ):
716713 imro_per_channel [field ].append (field_value )
717714
718- # Resolve activate electrodes (i.e. `electrodes` entry) for probe types whose IMRO format does not include them.
719- # NP2.x+ probes have "electrode" directly in the IMRO table. NP1.x probes encode
720- # electrode selection indirectly and need computation.
715+ return imro_per_channel
716+
717+
718+ def _get_active_contact_ids (imro_per_channel : dict , imro_table_string : str ) -> list [str ]:
719+ """
720+ Get canonical contact ID strings for the active electrodes in a parsed IMRO table.
721+
722+ If the IMRO format includes electrode IDs directly (NP2.x+), uses them as-is.
723+ If not (NP1.x), resolves them first via the appropriate addressing scheme
724+ (simple bank for NP1.0-like probes, UHD group-based for NP1110).
725+
726+ Parameters
727+ ----------
728+ imro_per_channel : dict
729+ Parsed IMRO data from _parse_imro_string. Modified in place if electrode
730+ IDs need to be resolved.
731+ imro_table_string : str
732+ Raw IMRO table string, needed by NP1110 to extract col_mode from the header.
733+
734+ Returns
735+ -------
736+ list of str
737+ Canonical contact ID strings (e.g., ["e0", "e384", ...] or ["s0e123", ...]).
738+ """
721739 if "electrode" not in imro_per_channel :
722740 _resolve_active_contacts_for_np1 (imro_per_channel )
723741 if "electrode" not in imro_per_channel :
724742 _resolve_active_contacts_for_np1110 (imro_per_channel , imro_table_string )
743+ assert "electrode" in imro_per_channel , (
744+ f"Could not resolve electrode IDs from IMRO fields: { list (imro_per_channel .keys ())} "
745+ )
725746
726- return imro_per_channel
747+ elec_ids = imro_per_channel ["electrode" ]
748+ shank_ids = imro_per_channel .get ("shank" , [None ] * len (elec_ids ))
749+ return [
750+ _build_canonical_contact_id (elec_id , shank_id )
751+ for shank_id , elec_id in zip (shank_ids , elec_ids )
752+ ]
727753
728754
729755def _resolve_active_contacts_for_np1 (imro_per_channel : dict ) -> None :
@@ -739,7 +765,7 @@ def _resolve_active_contacts_for_np1(imro_per_channel: dict) -> None:
739765 Parameters
740766 ----------
741767 imro_per_channel : dict
742- Parsed IMRO data from _interpret_imro_string . Modified in place.
768+ Parsed IMRO data from _parse_imro_string . Modified in place.
743769 """
744770 if "channel" not in imro_per_channel :
745771 return
@@ -784,7 +810,7 @@ def _resolve_active_contacts_for_np1110(imro_per_channel: dict, imro_table_strin
784810 "been validated against real NP1110 recordings. Please double-check the electrode "
785811 "selection and report any issues at https://github.com/SpikeInterface/probeinterface/issues" ,
786812 UserWarning ,
787- stacklevel = 3 , # Points to read_imro / read_spikeglx (caller of _interpret_imro_string )
813+ stacklevel = 3 , # Points to read_imro / read_spikeglx (caller of _ensure_active_contacts_available )
788814 )
789815
790816 # Extract col_mode from IMRO header: (type,col_mode,ref_id,ap_gain,lf_gain,ap_hipas_flt)
@@ -899,15 +925,10 @@ def read_spikeglx(file: str | Path) -> Probe:
899925 # Specifies which electrodes were selected for recording (e.g., 384 of 960) plus their
900926 # acquisition settings (gains, references, filters). See: https://billkarsh.github.io/SpikeGLX/help/imroTables/
901927 imro_table_string = meta ["imroTbl" ]
902- imro_per_channel = _interpret_imro_string (imro_table_string , imDatPrb_pn )
928+ imro_per_channel = _parse_imro_string (imro_table_string , imDatPrb_pn )
903929
904930 # ===== 4. Build contact IDs for active electrodes =====
905- # Convert physical electrode IDs to probeinterface canonical contact ID strings
906- imro_electrode = imro_per_channel ["electrode" ]
907- imro_shank = imro_per_channel .get ("shank" , [None ] * len (imro_electrode ))
908- active_contact_ids = [
909- _build_canonical_contact_id (elec_id , shank_id ) for shank_id , elec_id in zip (imro_shank , imro_electrode )
910- ]
931+ active_contact_ids = _get_active_contact_ids (imro_per_channel , imro_table_string )
911932
912933 # ===== 5. Slice full probe to active electrodes =====
913934 # Find indices of active contacts in the full probe, preserving IMRO order
@@ -944,7 +965,7 @@ def read_spikeglx(file: str | Path) -> Probe:
944965 if saved_chans .size != probe .get_contact_count ():
945966 probe = probe .get_slice (saved_chans )
946967
947- # ===== 6 . Add recording-specific annotations =====
968+ # ===== 8 . Add recording-specific annotations =====
948969 # These annotations identify the physical probe instance and recording setup
949970 imDatPrb_serial_number = meta .get ("imDatPrb_sn" ) or meta .get ("imProbeSN" ) # Phase3A uses imProbeSN
950971 imDatPrb_port = meta .get ("imDatPrb_port" , None )
@@ -954,7 +975,7 @@ def read_spikeglx(file: str | Path) -> Probe:
954975 probe .annotate (port = imDatPrb_port )
955976 probe .annotate (slot = imDatPrb_slot )
956977
957- # ===== 7 . Set device channel indices (wiring) =====
978+ # ===== 9 . Set device channel indices (wiring) =====
958979 probe .set_device_channel_indices (np .arange (probe .get_contact_count ()))
959980
960981 return probe
0 commit comments