@@ -693,6 +693,51 @@ def write_imro(file: str | Path, probe: Probe):
693693##
694694
695695
696+ def _parse_imro_string (imro_table_string : str , probe_part_number : str ) -> dict :
697+ """
698+ Parse IMRO (Imec ReadOut) table string into structured per-channel data.
699+
700+ IMRO format: "(probe_type,num_chans)(ch0 bank0 ref0 ...)(ch1 bank1 ref1 ...)..."
701+ Example: "(0,384)(0 1 0 500 250 1)(1 0 0 500 250 1)..."
702+
703+ Note: The IMRO header contains a probe_type field (e.g., "0", "21", "24"), which is
704+ a numeric format version identifier that specifies which IMRO table structure was used.
705+ Different probe generations use different IMRO formats. This is a file format detail,
706+ not a physical probe property.
707+
708+ Parameters
709+ ----------
710+ imro_table_string : str
711+ IMRO table string from SpikeGLX metadata file
712+ probe_part_number : str
713+ Probe part number (e.g., "NP1000", "NP2000")
714+
715+ Returns
716+ -------
717+ imro_per_channel : dict
718+ Dictionary where each key maps to a list of values (one per channel).
719+ 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,...]}
721+ """
722+ # Get IMRO field format from catalogue
723+ probe_features = _load_np_probe_features ()
724+ probe_spec = probe_features ["neuropixels_probes" ][probe_part_number ]
725+ imro_format = probe_spec ["imro_table_format_type" ]
726+ imro_fields_string = probe_features ["z_imro_formats" ][imro_format + "_elm_flds" ]
727+ imro_fields = tuple (imro_fields_string .replace ("(" , "" ).replace (")" , "" ).split (" " ))
728+
729+ # Parse IMRO table values into per-channel data
730+ # Skip the header "(probe_type,num_chans)" and trailing empty string
731+ _ , * imro_table_values_list , _ = imro_table_string .strip ().split (")" )
732+ imro_per_channel = {k : [] for k in imro_fields }
733+ for field_values_str in imro_table_values_list :
734+ values = tuple (map (int , field_values_str [1 :].split (" " )))
735+ for field , field_value in zip (imro_fields , values ):
736+ imro_per_channel [field ].append (field_value )
737+
738+ return imro_per_channel
739+
740+
696741def read_spikeglx (file : str | Path ) -> Probe :
697742 """
698743 Read probe geometry and configuration from a SpikeGLX metadata file.
@@ -739,73 +784,49 @@ def read_spikeglx(file: str | Path) -> Probe:
739784 full_probe = build_neuropixels_probe (probe_part_number = imDatPrb_pn )
740785
741786 # ===== 3. Parse IMRO table to extract recorded electrodes and acquisition settings =====
742- # The IMRO table specifies which electrodes were selected for recording (e.g., 384 of 960),
743- # plus their acquisition settings (gains, references, filters)
744- imro_table = meta ["imroTbl" ]
745- probe_type_num_chans , * imro_table_values_list , _ = imro_table .strip ().split (")" )
746-
747- # probe_type_num_chans looks like f"({probe_type},{num_chans}"
748- probe_type = probe_type_num_chans .split ("," )[0 ][1 :]
749-
750- probe_features = _load_np_probe_features ()
751- _ , imro_fields , _ = get_probe_metadata_from_probe_features (probe_features , imDatPrb_pn )
752-
753- # Parse IMRO table values
754- contact_info = {k : [] for k in imro_fields }
755- for field_values_str in imro_table_values_list : # Imro table values look like '(value, value, value, ... '
756- # Split them by space to get int('value'), int('value'), int('value'), ...)
757- values = tuple (map (int , field_values_str [1 :].split (" " )))
758- for field , field_value in zip (imro_fields , values ):
759- contact_info [field ].append (field_value )
760-
761- # Convert IMRO channel numbers to physical electrode IDs
762- # The IMRO table specifies which electrodes were recorded, but different probe types
763- # encode this information differently:
764- readout_channel_ids = np .array (contact_info ["channel" ]) # 0-383 for NP1.0
765-
766- if "electrode" in contact_info :
767- # NP2.0 and some probes directly specify the physical electrode ID
768- physical_electrode_ids = np .array (contact_info ["electrode" ])
787+ # IMRO = Imec ReadOut (the configuration table format from IMEC manufacturer)
788+ # Specifies which electrodes were selected for recording (e.g., 384 of 960) plus their
789+ # acquisition settings (gains, references, filters). See: https://billkarsh.github.io/SpikeGLX/help/imroTables/
790+ imro_table_string = meta ["imroTbl" ]
791+ imro_per_channel = _parse_imro_string (imro_table_string , imDatPrb_pn )
792+
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" ])
769799 else :
770- # NP1.0 uses banks to encode electrode position
771- # Physical electrode ID = bank * 384 + channel
772- # (e.g., bank 0, channel 0 → electrode 0; bank 1, channel 0 → electrode 384)
773- bank_key = "bank" if "bank" in contact_info else "bank_mask"
774- bank_indices = np .array (contact_info [bank_key ])
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 ])
775804 physical_electrode_ids = bank_indices * 384 + readout_channel_ids
776805
777- # ===== 4. Slice full probe to IMRO-selected electrodes =====
778- # The full probe has all electrodes (e.g., 960 for NP1.0). We need to find which
779- # indices in the full probe array correspond to the electrodes selected in the IMRO table.
806+ # ===== 5. Slice full probe to active electrodes =====
780807 selected_contact_indices = []
781-
782808 for idx , electrode_id in enumerate (physical_electrode_ids ):
783- # Build the contact ID string that matches the full probe's contact_ids array
784- if "shank" in contact_info :
785- # Multi-shank probes: contact ID = "s{shank}e{electrode}"
786- shank_id = contact_info ["shank" ][idx ]
809+ if "shank" in imro_per_channel :
810+ shank_id = imro_per_channel ["shank" ][idx ]
787811 contact_id_str = f"s{ shank_id } e{ electrode_id } "
788812 else :
789- # Single-shank probes: contact ID = "e{electrode}"
790813 contact_id_str = f"e{ electrode_id } "
791814
792- # Find where this contact appears in the full probe
793815 full_probe_index = np .where (full_probe .contact_ids == contact_id_str )[0 ]
794816 if len (full_probe_index ) > 0 :
795817 selected_contact_indices .append (full_probe_index [0 ])
796818
797819 probe = full_probe .get_slice (np .array (selected_contact_indices , dtype = int ))
798820
799- # Add IMRO-specific contact annotations (acquisition settings)
800- probe .annotate (probe_type = probe_type )
821+ # ===== 6. Store IMRO properties (acquisition settings) as annotations =====
801822 vector_properties = ("channel" , "bank" , "bank_mask" , "ref_id" , "ap_gain" , "lf_gain" , "ap_hipas_flt" )
802823 vector_properties_available = {}
803- for k , v in contact_info .items ():
824+ for k , v in imro_per_channel .items ():
804825 if (k in vector_properties ) and (len (v ) > 0 ):
805826 vector_properties_available [imro_field_to_pi_field .get (k )] = v
806827 probe .annotate_contacts (** vector_properties_available )
807828
808- # ===== 5 . Slice to saved channels (if subset was saved) =====
829+ # ===== 7 . Slice to saved channels (if subset was saved) =====
809830 # This is DIFFERENT from IMRO selection: IMRO selects which electrodes to acquire,
810831 # but SpikeGLX can optionally save only a subset of acquired channels to reduce file size.
811832 # For example: IMRO selects 384 electrodes, but only 300 are saved to disk.
0 commit comments