Skip to content

Commit 8d49927

Browse files
committed
respond to Heberto
1 parent 11db7ec commit 8d49927

2 files changed

Lines changed: 63 additions & 46 deletions

File tree

src/probeinterface/neuropixels_tools.py

Lines changed: 62 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
import warnings
1414
from packaging.version import parse
1515
import json
16-
1716
import numpy as np
1817

1918
from .probe import Probe
2019
from .utils import import_safely
2120

2221
# Map imDatPrb_pn (probe number) to imDatPrb_type (probe type) when the latter is missing
22+
# ONLY needed for `read_imro` function
2323
probe_part_number_to_probe_type = {
2424
# for old version without a probe number we assume NP1.0
2525
None: "0",
@@ -59,8 +59,7 @@
5959
"NP1300": "1300", # Opto probe
6060
}
6161

62-
probe_type_to_probe_part_number = {v: k for k, v in probe_part_number_to_probe_type.items()}
63-
62+
# Map from imro format to ProbeInterface naming conventions
6463
imro_field_to_pi_field = {
6564
"ap_gain": "ap_gains",
6665
"ap_hipas_flt": "ap_hp_filters",
@@ -76,6 +75,7 @@
7675
"bankB": "bankB",
7776
}
7877

78+
# Map from ProbeInterface to ProbeTable naming conventions
7979
pi_to_pt_names = {
8080
"x_pitch": "electrode_pitch_horz_um",
8181
"y_pitch": "electrode_pitch_vert_um",
@@ -91,6 +91,26 @@
9191
"tip_length_um": "tip_length_um",
9292
}
9393

94+
def get_probe_length(probe_part_number: str) -> int:
95+
"""
96+
Returns the length of a given probe. We assume a length of
97+
1cm (10_000 microns) by default.
98+
99+
Parameters
100+
----------
101+
probe_part_number : str
102+
The part number of the probe e.g. 'NP2013'.
103+
104+
Returns
105+
-------
106+
probe_length : int
107+
Lenth of full probe (microns)
108+
"""
109+
110+
probe_length = 10_000
111+
112+
return probe_length
113+
94114

95115
def make_npx_description(probe_part_number):
96116
"""
@@ -109,21 +129,15 @@ def make_npx_description(probe_part_number):
109129
Dictionary containing metadata about NeuroPixels probes using ProbeInterface syntax.
110130
"""
111131

112-
is_phase3a = False
113-
# These are all prototype NP1.0 probes, not contained in ProbeTable
114-
if probe_part_number in ["PRB_1_4_0480_1", "PRB_1_4_0480_1_C", "PRB_1_2_0480_2", None]:
115-
if probe_part_number is None:
116-
is_phase3a = True
117-
probe_part_number = "NP1010"
118-
119132
probe_features_filepath = Path(__file__).absolute().parent / Path("resources/probe_features.json")
120133
probe_features = json.load(open(probe_features_filepath, "r"))
134+
135+
# We use `pt` and `pi` as shorthand for `ProbeTable` and `ProbeInterface` throughout this function
121136
pt_metadata = probe_features["neuropixels_probes"].get(probe_part_number)
137+
pi_metadata = {}
122138

123139
if pt_metadata is None:
124-
raise ValueError(f"Probe type {probe_part_number} not supported.")
125-
126-
pi_metadata = {}
140+
raise ValueError(f"Probe part number {probe_part_number} not supported.")
127141

128142
# Extract most of the metadata
129143
for pi_name, pt_name in pi_to_pt_names.items():
@@ -159,14 +173,10 @@ def make_npx_description(probe_part_number):
159173
# Read the imro table formats to find out which fields the imro tables contain
160174
imro_table_format_type = pt_metadata["imro_table_format_type"]
161175
imro_table_fields = probe_features["z_imro_formats"][imro_table_format_type + "_elm_flds"]
162-
176+
163177
# parse the imro_table_fields, which look like (value value value ...)
164178
list_of_imro_fields = imro_table_fields.replace("(", "").replace(")", "").split(" ")
165179

166-
# The Phase3a probe does not contain the `ap_hipas_flt` imro table field.
167-
if is_phase3a:
168-
list_of_imro_fields.remove("ap_hipas_flt")
169-
170180
pi_imro_fields = []
171181
for imro_field in list_of_imro_fields:
172182
pi_imro_fields.append(imro_field_to_pi_field[imro_field])
@@ -175,10 +185,12 @@ def make_npx_description(probe_part_number):
175185
# Construct probe contour, for styling the probe
176186
shank_width = float(pt_metadata["shank_width_um"])
177187
tip_length = float(pt_metadata["tip_length_um"])
178-
probe_length = 10_000
188+
189+
probe_length = get_probe_length(probe_part_number)
179190
pi_metadata["contour_description"] = get_probe_contour_vertices(shank_width, tip_length, probe_length)
180191

181-
# Get the mux table
192+
# Get the mux table. This describes which electrodes are multiplexed together, meaning
193+
# which electrodes are sampled at the same time.
182194
mux_table_format_type = pt_metadata["mux_table_format_type"]
183195
mux_information = probe_features["z_mux_tables"].get(mux_table_format_type)
184196
pi_metadata["mux_table_array"] = make_mux_table_array(mux_information)
@@ -279,7 +291,25 @@ def read_imro(file_path: Union[str, Path]) -> Probe:
279291
assert meta_file.suffix == ".imro", "'file' should point to the .imro file"
280292
with meta_file.open(mode="r") as f:
281293
imro_str = str(f.read())
282-
return _read_imro_string(imro_str)
294+
295+
imro_table_header_str, *imro_table_values_list, _ = imro_str.strip().split(")")
296+
imro_table_header = tuple(map(int, imro_table_header_str[1:].split(",")))
297+
298+
if len(imro_table_header) == 3:
299+
# In older versions of neuropixel arrays (phase 3A), imro tables were structured differently.
300+
# We use probe_type "0", which maps to probe_part_number NP1010 as a proxy for Phase3a.
301+
imDatPrb_type = "0"
302+
elif len(imro_table_header) == 2:
303+
imDatPrb_type, _ = imro_table_header
304+
else:
305+
raise ValueError(f"read_imro error, the header has a strange length: {imro_table_header}")
306+
imDatPrb_type = str(imDatPrb_type)
307+
308+
for probe_part_number, probe_type in probe_part_number_to_probe_type.items():
309+
if imDatPrb_type == probe_type:
310+
imDatPrb_pn = probe_part_number
311+
312+
return _read_imro_string(imro_str, imDatPrb_pn)
283313

284314

285315
def _make_npx_probe_from_description(probe_description, elec_ids, shank_ids):
@@ -369,30 +399,19 @@ def _read_imro_string(imro_str: str, imDatPrb_pn: Optional[str] = None) -> Probe
369399
https://billkarsh.github.io/SpikeGLX/help/imroTables/
370400
371401
"""
372-
imro_table_header_str, *imro_table_values_list, _ = imro_str.strip().split(")")
373-
imro_table_header = tuple(map(int, imro_table_header_str[1:].split(",")))
374402

375-
imDatPrb_type = None
376-
if imDatPrb_pn is None:
377-
if len(imro_table_header) == 3:
378-
# In older versions of neuropixel arrays (phase 3A), imro tables were structured differently.
379-
probe_serial_number, probe_option, num_contact = imro_table_header
380-
imDatPrb_type = "Phase3a"
381-
imDatPrb_pn = None
382-
elif len(imro_table_header) == 2:
383-
imDatPrb_type, num_contact = imro_table_header
384-
imDatPrb_type = str(imDatPrb_type)
385-
imDatPrb_pn = probe_type_to_probe_part_number[imDatPrb_type]
386-
else:
387-
raise ValueError(f"read_imro error, the header has a strange length: {imro_table_header}")
403+
probe_type_num_chans, *imro_table_values_list, _ = imro_str.strip().split(")")
404+
405+
# probe_type_num_chans looks like f"({probe_type},{num_chans}"
406+
probe_type = probe_type_num_chans.split(',')[0][1:]
388407

389408
probe_description = make_npx_description(imDatPrb_pn)
390409

391410
fields = probe_description["fields_in_imro_table"]
392411
contact_info = {k: [] for k in fields}
393412
for field_values_str in imro_table_values_list: # Imro table values look like '(value, value, value, ... '
413+
# Split them by space to get int('value'), int('value'), int('value'), ...)
394414
values = tuple(map(int, field_values_str[1:].split(" ")))
395-
# Split them by space to get (int('value'), int('value'), int('value'), ...)
396415
for field, field_value in zip(fields, values):
397416
contact_info[field].append(field_value)
398417

@@ -416,12 +435,12 @@ def _read_imro_string(imro_str: str, imDatPrb_pn: Optional[str] = None) -> Probe
416435

417436
# this is scalar annotations
418437
probe.annotate(
419-
probe_type=imDatPrb_type,
438+
probe_type=probe_type,
420439
)
421440

422441
# this is vector annotations
423442
vector_properties = ("channel_ids", "banks", "references", "ap_gains", "lf_gains", "ap_hp_filters")
424-
vector_properties_available = {k: v for k, v in contact_info.items() if k in vector_properties}
443+
vector_properties_available = {k: v for k, v in contact_info.items() if (k in vector_properties) and (len(v)>0) }
425444
probe.annotate_contacts(**vector_properties_available)
426445

427446
return probe
@@ -494,12 +513,6 @@ def read_spikeglx(file: str | Path) -> Probe:
494513
495514
The shape is auto generated as a shank.
496515
497-
Now reads:
498-
* NP0.0 (=phase3A)
499-
* NP1.0 (=phase3B2)
500-
* NP2.0 with 4 shank
501-
* NP1.0-NHP
502-
503516
Parameters
504517
----------
505518
file : Path or str
@@ -530,6 +543,10 @@ def read_spikeglx(file: str | Path) -> Probe:
530543
imDatPrb_slot = meta.get("imDatPrb_slot", None)
531544
imDatPrb_part_number = meta.get("imDatPrb_pn", None)
532545

546+
# Only Phase3a probe has "imProbeOpt". Map this to NP10101.
547+
if meta.get("imProbeOpt") is not None:
548+
imDatPrb_pn = "NP1010"
549+
533550
probe = _read_imro_string(imro_str=imro_table, imDatPrb_pn=imDatPrb_pn)
534551

535552
# add serial number and other annotations

src/probeinterface/resources/probe_features.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1886,7 +1886,7 @@
18861886
"tip_length_um": "206",
18871887
"total_electrodes": "5120"
18881888
},
1889-
"PRB_1_2_480_2": {
1889+
"PRB_1_2_0480_2": {
18901890
"adc_bit_depth": "10",
18911891
"adc_range_vpp": "1.2",
18921892
"ap_band_list_hz": "300,10000",

0 commit comments

Comments
 (0)