Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 22 additions & 38 deletions src/probeinterface/neuropixels_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,54 +593,38 @@ def _read_imro_string(imro_str: str, imDatPrb_pn: Optional[str] = None) -> Probe

"""

probe_type_num_chans, *imro_table_values_list, _ = imro_str.strip().split(")")
# Extract probe_type from the IMRO header "(probe_type,num_chans)"
probe_type = imro_str.strip().split(")")[0].split(",")[0][1:]

# probe_type_num_chans looks like f"({probe_type},{num_chans}"
probe_type = probe_type_num_chans.split(",")[0][1:]
# Parse the IMRO table into per-channel data (same parser used by read_spikeglx)
imro_per_channel = _parse_imro_string(imro_str, imDatPrb_pn)

probe_features = _load_np_probe_features()
pt_metadata, fields, mux_info = get_probe_metadata_from_probe_features(probe_features, imDatPrb_pn)
# Build full catalogue probe and slice to active electrodes
full_probe = build_neuropixels_probe(probe_part_number=imDatPrb_pn)

# fields = probe_description["fields_in_imro_table"]
contact_info = {k: [] for k in fields}
for field_values_str in imro_table_values_list: # Imro table values look like '(value, value, value, ... '
# Split them by space to get int('value'), int('value'), int('value'), ...)
values = tuple(map(int, field_values_str[1:].split(" ")))
for field, field_value in zip(fields, values):
contact_info[field].append(field_value)
elec_ids = imro_per_channel["electrode"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think imro_np1000 and imro_np1110 don't have electrode as a key (

"imro_np1000_elm_flds": "(channel bank ref_id ap_gain lf_gain ap_hipas_flt)",
)
which is what all bank-related logic below (deleted lines 613-621) refer to. So you need to keep that logic in. Annoyingly, we don't have test data for this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bank-y logic should be added to read_spikeglx too, I think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

@h-mayorquin h-mayorquin Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Neuropixels 1.0 I codified the fallback in a new function _resolve_active_contacts_for_np1 that encapsulates the electrode = bank * 384 + channel logic. Both read_spikeglx and read_imro now call _get_active_contact_ids which resolves electrode IDs if missing and builds the canonical contact ID strings. This function dispatches to the appropriate resolver based on the IMRO fields present. The IMRO parsing itself stays in _parse_imro_string, which is now a pure parser that returns a dict of IMRO fields without any electrode resolution (the electrode computation that was previously inline there has been extracted into the resolver functions).

For NP1110 I went and checked how it is done in the C++ implementation (IMROTbl_T1110.cpp in the SpikeGLX repo). I threw an agent at it and had it implement the same logic here, which is now in _resolve_active_contacts_for_np1110. I added enough caveats in the function (a warning, a TODO, a comment explaining why we're mirroring the C++ naming) until we get real test data, but I think it should be OK to move forward.

I also inlined all the logic for building the probe into read_imro directly so it is even more like read_spikeglx, instead of relying on a private method that was doing most of the computation.

shank_ids = imro_per_channel.get("shank", [None] * len(elec_ids))
active_contact_ids = [
_build_canonical_contact_id(elec_id, shank_id) for shank_id, elec_id in zip(shank_ids, elec_ids)
]

channel_ids = np.array(contact_info["channel"])
if "electrode" in contact_info:
elec_ids = np.array(contact_info["electrode"])
else:
if contact_info.get("bank") is not None:
bank_key = "bank"
elif contact_info.get("bank_mask") is not None:
bank_key = "bank_mask"
banks = np.array(contact_info[bank_key])
elec_ids = banks * 384 + channel_ids

if pt_metadata["num_shanks"] > 1:
shank_ids = np.array(contact_info["shank"])
else:
shank_ids = None
contact_id_to_index = {cid: i for i, cid in enumerate(full_probe.contact_ids)}
selected_indices = np.array([contact_id_to_index[cid] for cid in active_contact_ids])
probe = full_probe.get_slice(selected_indices)

probe = _make_npx_probe_from_description(pt_metadata, imDatPrb_pn, elec_ids, shank_ids, mux_info)
# ADC sampling annotations
adc_sampling_table = probe.annotations.get("adc_sampling_table")
_annotate_probe_with_adc_sampling_info(probe, adc_sampling_table)

# scalar annotations
probe.annotate(
probe_type=probe_type,
)
# Scalar annotations
probe.annotate(probe_type=probe_type)

# vector annotations
# Vector annotations from IMRO fields
vector_properties = ("channel", "bank", "bank_mask", "ref_id", "ap_gain", "lf_gain", "ap_hipas_flt")

vector_properties_available = {}
for k, v in contact_info.items():
if (k in vector_properties) and (len(v) > 0):
# convert to ProbeInterface naming for backwards compatibility
for k, v in imro_per_channel.items():
if k in vector_properties and len(v) > 0:
vector_properties_available[imro_field_to_pi_field.get(k)] = v

probe.annotate_contacts(**vector_properties_available)

return probe
Expand Down
Loading