Skip to content

Commit adc7856

Browse files
committed
naming
1 parent fd1c152 commit adc7856

1 file changed

Lines changed: 51 additions & 30 deletions

File tree

src/probeinterface/neuropixels_tools.py

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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

729755
def _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

Comments
 (0)