Skip to content

Commit f200342

Browse files
committed
imro parsing
1 parent 36be317 commit f200342

File tree

1 file changed

+67
-46
lines changed

1 file changed

+67
-46
lines changed

src/probeinterface/neuropixels_tools.py

Lines changed: 67 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
696741
def 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

Comments
 (0)